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));