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

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 46655500d8 GROOVY-12147: Add locale-aware number, currency and percent 
formatting/parsing to the GDK (back out some variants and clarify javadoc)
46655500d8 is described below

commit 46655500d81c3e4685269759f1e27748dbcd5f05
Author: Paul King <[email protected]>
AuthorDate: Mon Jul 13 11:09:14 2026 +1000

    GROOVY-12147: Add locale-aware number, currency and percent 
formatting/parsing to the GDK (back out some variants and clarify javadoc)
---
 .../groovy/runtime/DefaultGroovyMethods.java       | 60 ++++++++++++++++--
 .../groovy/runtime/StringGroovyMethods.java        | 72 ++++++++++------------
 .../groovy/runtime/StringGroovyMethodsTest.java    | 14 +++--
 3 files changed, 98 insertions(+), 48 deletions(-)

diff --git 
a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java 
b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
index b73727b319..f9b0f3cc9a 100644
--- a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
@@ -13378,10 +13378,15 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
 
     /**
      * Printf to the standard output stream.
+     * Formatting is locale-sensitive and uses {@link Locale#getDefault()}. To 
format
+     * for an explicit locale, print the result of
+     * {@link String#format(Locale, String, Object...)} instead.
      *
      * @param self   any Object
      * @param format a format string
      * @param values values referenced by the format specifiers in the format 
string
+     *
+     * @see java.lang.String#format(Locale, String, Object...)
      * @since 1.0
      */
     public static void printf(Object self, String format, Object[] values) {
@@ -13394,10 +13399,15 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
     /**
      * Printf 0 or more values to the standard output stream using a format 
string.
      * This method delegates to the owner to execute the method.
+     * Formatting is locale-sensitive and uses {@link Locale#getDefault()}. To 
format
+     * for an explicit locale, print the result of
+     * {@link String#format(Locale, String, Object...)} instead.
      *
      * @param self   a generated closure
      * @param format a format string
      * @param values values referenced by the format specifiers in the format 
string
+     *
+     * @see java.lang.String#format(Locale, String, Object...)
      * @since 3.0.0
      */
     public static void printf(Closure self, String format, Object[] values) {
@@ -13411,10 +13421,15 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
     /**
      * Printf a value to the standard output stream using a format string.
      * This method delegates to the owner to execute the method.
+     * Formatting is locale-sensitive and uses {@link Locale#getDefault()}. To 
format
+     * for an explicit locale, print the result of
+     * {@link String#format(Locale, String, Object...)} instead.
      *
      * @param self   a generated closure
      * @param format a format string
      * @param value  value referenced by the format specifier in the format 
string
+     *
+     * @see java.lang.String#format(Locale, String, Object...)
      * @since 3.0.0
      */
     public static void printf(Closure self, String format, Object value) {
@@ -13444,12 +13459,17 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
      *     ( 7..11 ).each { printf ( "-- %5.2f\n" , [ it ] as float[] ) }
      *     ( 7..11 ).each { printf ( "-- %5.2g\n" , [ it ] as double[] ) }
      * </pre>
+     * Formatting is locale-sensitive and uses {@link Locale#getDefault()}. To 
format
+     * for an explicit locale, print the result of
+     * {@link String#format(Locale, String, Object...)} instead.
      *
      * @param self   any Object
      * @param format A format string
      * @param arg    Argument which is referenced by the format specifiers in 
the format
      *               string.  The type of <code>arg</code> should be one of 
Object[], List,
      *               int[], short[], byte[], char[], boolean[], long[], 
float[], or double[].
+     *
+     * @see java.lang.String#format(Locale, String, Object...)
      * @since 1.0
      */
     public static void printf(Object self, String format, Object arg) {
@@ -15299,11 +15319,15 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
 
     /**
      * Sprintf to a string.
+     * Formatting is locale-sensitive and uses {@link Locale#getDefault()}. To 
format
+     * for an explicit locale, use {@link String#format(Locale, String, 
Object...)} instead.
      *
      * @param self   any Object
      * @param format a format string
      * @param values values referenced by the format specifiers in the format 
string
      * @return the resulting formatted string
+     *
+     * @see java.lang.String#format(Locale, String, Object...)
      * @since 1.5.0
      */
     public static String sprintf(Object self, String format, Object[] values) {
@@ -15316,6 +15340,8 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
     /**
      * Returns a formatted string using the specified format string and
      * arguments.
+     * Formatting is locale-sensitive and uses {@link Locale#getDefault()}. To 
format
+     * for an explicit locale, use {@link String#format(Locale, String, 
Object...)} instead.
      *
      * @param self   any Object
      * @param format A format string
@@ -15323,6 +15349,8 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
      *               string.  The type of <code>arg</code> should be one of 
Object[], List,
      *               int[], short[], byte[], char[], boolean[], long[], 
float[], or double[].
      * @return the resulting printf'd string
+     *
+     * @see java.lang.String#format(Locale, String, Object...)
      * @since 1.5.0
      */
     public static String sprintf(Object self, String format, Object arg) {
@@ -16509,10 +16537,17 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
     // toCurrencyString
 
     /**
-     * Formats a Number as a currency String using the default locale.
+     * Formats a Number as a currency String using {@link Locale#getDefault()}.
+     * This is intended for human-facing output in single-user contexts such 
as a
+     * CLI or desktop application. In a server or other multi-tenant context, 
the
+     * default locale is the container's, not the end user's, so prefer
+     * {@link #toCurrencyString(Number, Locale)} with an explicit locale.
      *
      * @param self a Number
      * @return the currency-formatted String
+     *
+     * @see #toCurrencyString(Number, Locale)
+     * @see java.text.NumberFormat#getCurrencyInstance()
      * @since 6.0.0
      */
     public static String toCurrencyString(Number self) {
@@ -16528,6 +16563,9 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
      * @param self   a Number
      * @param locale the locale defining the currency format
      * @return the currency-formatted String
+     *
+     * @see StringGroovyMethods#toCurrencyNumber(CharSequence, Locale)
+     * @see java.text.NumberFormat#getCurrencyInstance(Locale)
      * @since 6.0.0
      */
     public static String toCurrencyString(Number self, Locale locale) {
@@ -16538,11 +16576,19 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
     // toPercentString
 
     /**
-     * Formats a Number as a percent String using the default locale.
-     * The value is scaled by 100 (e.g. {@code 0.945} becomes {@code 94.5%}).
+     * Formats a Number as a percent String using {@link Locale#getDefault()}.
+     * The value is scaled by 100 and, by default, rounded to whole percent
+     * (e.g. {@code 0.945} becomes {@code 94%}).
+     * This is intended for human-facing output in single-user contexts such 
as a
+     * CLI or desktop application. In a server or other multi-tenant context, 
the
+     * default locale is the container's, not the end user's, so prefer
+     * {@link #toPercentString(Number, Locale)} with an explicit locale.
      *
      * @param self a Number
      * @return the percent-formatted String
+     *
+     * @see #toPercentString(Number, Locale)
+     * @see java.text.NumberFormat#getPercentInstance()
      * @since 6.0.0
      */
     public static String toPercentString(Number self) {
@@ -16551,14 +16597,20 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
 
     /**
      * Formats a Number as a percent String using the given locale.
-     * The value is scaled by 100 (e.g. {@code 0.945} becomes {@code 94.5%}).
+     * The value is scaled by 100 and, by default, rounded to whole percent.
      * <pre class="groovyTestCase">
      * assert 0.5.toPercentString(Locale.US) == '50%'
+     * assert 0.945.toPercentString(Locale.US) == '94%' // rounded, not '94.5%'
      * </pre>
+     * For fraction digits, format via {@link 
java.text.NumberFormat#getPercentInstance(Locale)}
+     * with {@link java.text.NumberFormat#setMaximumFractionDigits(int)}.
      *
      * @param self   a Number
      * @param locale the locale defining the percent format
      * @return the percent-formatted String
+     *
+     * @see StringGroovyMethods#toPercentNumber(CharSequence, Locale)
+     * @see java.text.NumberFormat#getPercentInstance(Locale)
      * @since 6.0.0
      */
     public static String toPercentString(Number self, Locale locale) {
diff --git a/src/main/java/org/codehaus/groovy/runtime/StringGroovyMethods.java 
b/src/main/java/org/codehaus/groovy/runtime/StringGroovyMethods.java
index 2d7f8248ae..306f094a0f 100644
--- a/src/main/java/org/codehaus/groovy/runtime/StringGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/StringGroovyMethods.java
@@ -3472,16 +3472,24 @@ public class StringGroovyMethods extends 
DefaultGroovyMethodsSupport {
     /**
      * Parses a CharSequence into a Number using the given locale, honouring
      * locale-specific grouping and decimal symbols. Like {@link 
#toBigDecimal(CharSequence)},
-     * parsing is strict: the entire (trimmed) CharSequence must be a valid 
number.
+     * parsing is strict: the entire (trimmed) CharSequence must be a valid 
number,
+     * and the result is an exact {@link BigDecimal}.
      * <pre class="groovyTestCase">
      * assert '1.234,5'.toNumber(Locale.GERMANY) == 1234.5
      * </pre>
+     * There is no locale-aware formatting counterpart: use
+     * {@link String#format(Locale, String, Object...)}, e.g. {@code 
String.format(Locale.GERMANY, '%,.3f', n)}.
+     * Note that unlike this method, such formatting rounds to the given 
number of
+     * fraction digits; {@link Object#toString()} gives the exact, locale-free 
form.
      *
      * @param self   a CharSequence
      * @param locale the locale defining the number symbols
-     * @return the parsed Number
+     * @return the parsed Number (a BigDecimal)
      * @throws NumberFormatException if the CharSequence cannot be parsed in 
full
      *
+     * @see #toBigDecimal(CharSequence)
+     * @see java.lang.String#format(Locale, String, Object...)
+     * @see java.text.NumberFormat#getNumberInstance(Locale)
      * @since 6.0.0
      */
     public static Number toNumber(final CharSequence self, final Locale 
locale) {
@@ -3491,8 +3499,11 @@ public class StringGroovyMethods extends 
DefaultGroovyMethodsSupport {
     /**
      * Parses a CharSequence in full using the given format, throwing a
      * {@link NumberFormatException} if the trimmed text is not entirely 
consumed.
+     * The value is parsed as an exact {@link BigDecimal}, matching the decimal
+     * semantics of Groovy's own literals.
      */
-    private static Number parseFully(final NumberFormat format, final 
CharSequence self, final String kind) {
+    private static BigDecimal parseFully(final NumberFormat format, final 
CharSequence self, final String kind) {
+        if (format instanceof DecimalFormat) ((DecimalFormat) 
format).setParseBigDecimal(true);
         String text = self.toString().trim();
         ParsePosition pos = new ParsePosition(0);
         Number result = format.parse(text, pos);
@@ -3500,21 +3511,8 @@ public class StringGroovyMethods extends 
DefaultGroovyMethodsSupport {
             int at = result == null ? pos.getErrorIndex() : pos.getIndex();
             throw new NumberFormatException("Unparseable " + kind + ": \"" + 
self + "\" (at index " + at + ")");
         }
-        return result;
-    }
-
-    /**
-     * Parses a CharSequence into a Number using the currency format of the 
default locale.
-     * The result is an exact {@link BigDecimal}.
-     *
-     * @param self a CharSequence
-     * @return the parsed Number (a BigDecimal)
-     * @throws NumberFormatException if the CharSequence cannot be parsed
-     *
-     * @since 6.0.0
-     */
-    public static Number toCurrencyNumber(final CharSequence self) {
-        return toCurrencyNumber(self, Locale.getDefault());
+        // guarantee the documented BigDecimal type even for non-DecimalFormat 
providers
+        return (result instanceof BigDecimal) ? (BigDecimal) result : new 
BigDecimal(result.toString());
     }
 
     /**
@@ -3523,48 +3521,44 @@ public class StringGroovyMethods extends 
DefaultGroovyMethodsSupport {
      * <pre class="groovyTestCase">
      * assert '$1,234.50'.toCurrencyNumber(Locale.US) == 1234.50g
      * </pre>
+     * No default-locale variant is provided. Input carries the locale of 
wherever it
+     * came from (a file, a request, a database), which is less often the JVM 
default
+     * than is the case for human-facing output. Where the default locale is a 
fair
+     * assumption, such as a console application reading what the user typed, 
pass
+     * {@link Locale#getDefault()} explicitly.
      *
      * @param self   a CharSequence
      * @param locale the locale defining the currency format
      * @return the parsed Number (a BigDecimal)
      * @throws NumberFormatException if the CharSequence cannot be parsed
      *
+     * @see DefaultGroovyMethods#toCurrencyString(Number, Locale)
+     * @see java.text.NumberFormat#getCurrencyInstance(Locale)
      * @since 6.0.0
      */
     public static Number toCurrencyNumber(final CharSequence self, final 
Locale locale) {
-        NumberFormat nf = NumberFormat.getCurrencyInstance(locale);
-        if (nf instanceof DecimalFormat) ((DecimalFormat) 
nf).setParseBigDecimal(true);
-        Number result = parseFully(nf, self, "currency");
-        // guarantee the documented BigDecimal type even for non-DecimalFormat 
providers
-        return (result instanceof BigDecimal) ? result : new 
BigDecimal(result.toString());
-    }
-
-    /**
-     * Parses a CharSequence into a Number using the percent format of the 
default locale.
-     * The percent value is unscaled (divided by 100).
-     *
-     * @param self a CharSequence
-     * @return the parsed Number
-     * @throws NumberFormatException if the CharSequence cannot be parsed
-     *
-     * @since 6.0.0
-     */
-    public static Number toPercentNumber(final CharSequence self) {
-        return toPercentNumber(self, Locale.getDefault());
+        return parseFully(NumberFormat.getCurrencyInstance(locale), self, 
"currency");
     }
 
     /**
      * Parses a CharSequence into a Number using the percent format of the 
given locale.
-     * The percent value is unscaled (divided by 100).
+     * The percent value is unscaled (divided by 100). The result is an exact 
{@link BigDecimal}.
      * <pre class="groovyTestCase">
      * assert '50%'.toPercentNumber(Locale.US) == 0.5
      * </pre>
+     * No default-locale variant is provided. Input carries the locale of 
wherever it
+     * came from (a file, a request, a database), which is less often the JVM 
default
+     * than is the case for human-facing output. Where the default locale is a 
fair
+     * assumption, such as a console application reading what the user typed, 
pass
+     * {@link Locale#getDefault()} explicitly.
      *
      * @param self   a CharSequence
      * @param locale the locale defining the percent format
-     * @return the parsed Number
+     * @return the parsed Number (a BigDecimal)
      * @throws NumberFormatException if the CharSequence cannot be parsed
      *
+     * @see DefaultGroovyMethods#toPercentString(Number, Locale)
+     * @see java.text.NumberFormat#getPercentInstance(Locale)
      * @since 6.0.0
      */
     public static Number toPercentNumber(final CharSequence self, final Locale 
locale) {
diff --git 
a/src/test/groovy/org/codehaus/groovy/runtime/StringGroovyMethodsTest.java 
b/src/test/groovy/org/codehaus/groovy/runtime/StringGroovyMethodsTest.java
index 68755760a0..2cb669e7d3 100644
--- a/src/test/groovy/org/codehaus/groovy/runtime/StringGroovyMethodsTest.java
+++ b/src/test/groovy/org/codehaus/groovy/runtime/StringGroovyMethodsTest.java
@@ -266,9 +266,11 @@ public final class StringGroovyMethodsTest {
 
     @Test
     public void testToNumberWithLocale() {
-        assertEquals(1234.5, StringGroovyMethods.toNumber("1,234.5", 
Locale.US).doubleValue(), 0.0);
-        assertEquals(1234.5, StringGroovyMethods.toNumber("1.234,5", 
Locale.GERMANY).doubleValue(), 0.0);
-        assertEquals(42.0, StringGroovyMethods.toNumber("  42  ", 
Locale.US).doubleValue(), 0.0); // surrounding whitespace trimmed
+        Number us = StringGroovyMethods.toNumber("1,234.5", Locale.US);
+        assertEquals(new BigDecimal("1234.5"), us);
+        assertTrue(us instanceof BigDecimal, "should parse to an exact 
BigDecimal");
+        assertEquals(new BigDecimal("1234.5"), 
StringGroovyMethods.toNumber("1.234,5", Locale.GERMANY));
+        assertEquals(new BigDecimal("42"), StringGroovyMethods.toNumber("  42  
", Locale.US)); // surrounding whitespace trimmed
         assertThrows(NumberFormatException.class, () -> 
StringGroovyMethods.toNumber("abc", Locale.US));
         // strict: the whole input must parse, trailing junk is rejected (not 
silently truncated)
         assertThrows(NumberFormatException.class, () -> 
StringGroovyMethods.toNumber("12abc", Locale.US));
@@ -290,9 +292,11 @@ public final class StringGroovyMethodsTest {
 
     @Test
     public void testToPercentNumberWithLocale() {
-        assertEquals(0.5, StringGroovyMethods.toPercentNumber("50%", 
Locale.US).doubleValue(), 0.0);
+        Number us = StringGroovyMethods.toPercentNumber("50%", Locale.US);
+        assertEquals(0, new BigDecimal("0.5").compareTo((BigDecimal) us));
+        assertTrue(us instanceof BigDecimal, "percent should parse to an exact 
BigDecimal");
         // Turkish places the percent sign before the number
-        assertEquals(0.5, StringGroovyMethods.toPercentNumber("%50", new 
Locale("tr", "TR")).doubleValue(), 0.0);
+        assertEquals(0.5, StringGroovyMethods.toPercentNumber("%50", 
Locale.forLanguageTag("tr-TR")).doubleValue(), 0.0);
         assertThrows(NumberFormatException.class, () -> 
StringGroovyMethods.toPercentNumber("x", Locale.US));
         // strict: trailing text after the percent is rejected
         assertThrows(NumberFormatException.class, () -> 
StringGroovyMethods.toPercentNumber("50% off", Locale.US));

Reply via email to