[
https://issues.apache.org/jira/browse/GROOVY-12147?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King updated GROOVY-12147:
-------------------------------
Description:
h2. Summary
EDIT: updated to match what it would be after PR#2699
Groovy can already format and parse numbers, but only in a locale-invariant
way. {{String.format(locale, ...)}} / {{sprintf}} cover locale-aware
_grouped/decimal_ output, and {{"1234.50".toBigDecimal()}} covers _canonical_
parsing — but there is no idiomatic way to:
* format a number as *locale currency* ({{"€1.234,50"}}) or *locale percent*
({{"94,5 %"}}), or
* parse a *locale-formatted* number, currency or percent string back to a
{{Number}}.
These are exactly the cases {{java.util.Formatter}} ({{%f}}, {{%%}})
structurally cannot express (no currency conversion; {{%%}} is a literal sign,
so no ÷100 scaling and wrong symbol placement in locales such as Turkish
{{%94,5}} or German {{94,5 %}}). This issue adds a small set of GDK extension
methods that wrap {{java.text.NumberFormat}} to close that gap.
h2. Motivation
* {{StringGroovyMethods}} conversions ({{toInteger}}, {{toDouble}},
{{toBigDecimal}}, {{isNumber}}, …) are locale-invariant by construction ({{new
BigDecimal(str)}} / {{Double.valueOf(str)}}) — they reject grouping separators,
foreign decimal marks, currency symbols and percent signs.
* {{sprintf}}/{{Formatter}} handles grouped/decimal numbers and localized
{{%t}} dates, so no new date/time or {{sprintf}} surface is needed.
* The only genuinely missing capability is currency/percent formatting and
locale-aware parsing — a handful of thin {{NumberFormat}} wrappers.
h2. Proposed API
*Formatting* — extension methods on {{Number}} (in {{DefaultGroovyMethods}}):
{code:java}
public static String toCurrencyString(Number self)
public static String toCurrencyString(Number self, Locale locale)
public static String toPercentString(Number self)
public static String toPercentString(Number self, Locale locale)
{code}
*Parsing* — extension methods on {{CharSequence}} (in {{StringGroovyMethods}},
alongside {{toBigDecimal}}):
{code:java}
public static Number toNumber(CharSequence self, Locale locale)
// public static Number toCurrencyNumber(CharSequence self) // EDIT: removed
public static Number toCurrencyNumber(CharSequence self, Locale locale)
// public static Number toPercentNumber(CharSequence self) // EDIT: removed
public static Number toPercentNumber(CharSequence self, Locale locale)
{code}
All actually return {{BigDecimal}}.
h3. Examples
Formatting and parsing round-trip for a given locale:
{code:groovy}
assert 1234.5.toCurrencyString(Locale.US) == '$1,234.50'
assert '$1,234.50'.toCurrencyNumber(Locale.US) == 1234.50G
assert 0.5.toPercentString(Locale.US) == '50%'
assert '50%'.toPercentNumber(Locale.US) == 0.5
assert '1.234,5'.toNumber(Locale.GERMANY) == 1234.5
{code}
Locale-specific symbols and layout are honoured. Note that the JDK separates the
number from the currency/percent symbol with a *non-breaking space* (U+00A0), so
locale-formatted strings should not be hand-written with a plain space:
{code:groovy}
def de = Locale.GERMANY
assert 1234.5.toCurrencyString(de) == "1.234,50\u00A0\u20AC"
assert "1.234,50\u00A0\u20AC".toCurrencyNumber(de) == 1234.50G
{code}
h4. Contracts
* *Parsing requires an explicit Locale.* There is no default-locale overload:
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 is a fair assumption, such as a console
application reading what the user typed, pass {{Locale.getDefault()}}
explicitly.
* *Parsing is strict and exact.* The entire (trimmed) input must be consumed, so
trailing junk is rejected rather than silently truncated, and the result is an
exact {{BigDecimal}}:
{code:groovy}
import static groovy.test.GroovyAssert.shouldFail
assert '50%'.toPercentNumber(Locale.US) instanceof BigDecimal
shouldFail(NumberFormatException) { '$1,234.50 and
more'.toCurrencyNumber(Locale.US) }
{code}
* Formatting rounds; parsing does not.* {{NumberFormat}}'s percent instance
defaults to zero fraction digits, so the number-to-String direction is lossy:
{code:groovy}
assert 0.945.toPercentString(Locale.US) == '94%' // rounded, not '94.5%'
assert '94.5%'.toPercentNumber(Locale.US) == 0.945 // parsing keeps the
digits
{code}
For fraction digits, use {{NumberFormat.getPercentInstance(locale)}} with
{{setMaximumFractionDigits(int)}} directly. For locale-aware formatting of a
plain number, use {{String.format(locale, '%,.3f', n)}} — the GDK deliberately
adds no {{toNumberString}}, since {{String.format}} already covers it.
h2. Tests
* Round-trip format→parse for representative locales (US, Germany, France,
Turkey — Turkey to cover leading {{%}} placement).
* Percent parse applies ÷100 scaling.
* Malformed input throws {{NumberFormatException}}.
was:
h2. Summary
EDIT: updated to match what it would be after PR#2699
Groovy can already format and parse numbers, but only in a locale-invariant
way. {{String.format(locale, ...)}} / {{sprintf}} cover locale-aware
_grouped/decimal_ output, and {{"1234.50".toBigDecimal()}} covers _canonical_
parsing — but there is no idiomatic way to:
* format a number as *locale currency* ({{"€1.234,50"}}) or *locale percent*
({{"94,5 %"}}), or
* parse a *locale-formatted* number, currency or percent string back to a
{{Number}}.
These are exactly the cases {{java.util.Formatter}} ({{%f}}, {{%%}})
structurally cannot express (no currency conversion; {{%%}} is a literal sign,
so no ÷100 scaling and wrong symbol placement in locales such as Turkish
{{%94,5}} or German {{94,5 %}}). This issue adds a small set of GDK extension
methods that wrap {{java.text.NumberFormat}} to close that gap.
h2. Motivation
* {{StringGroovyMethods}} conversions ({{toInteger}}, {{toDouble}},
{{toBigDecimal}}, {{isNumber}}, …) are locale-invariant by construction ({{new
BigDecimal(str)}} / {{Double.valueOf(str)}}) — they reject grouping separators,
foreign decimal marks, currency symbols and percent signs.
* {{sprintf}}/{{Formatter}} handles grouped/decimal numbers and localized
{{%t}} dates, so no new date/time or {{sprintf}} surface is needed.
* The only genuinely missing capability is currency/percent formatting and
locale-aware parsing — a handful of thin {{NumberFormat}} wrappers.
h2. Proposed API
*Formatting* — extension methods on {{Number}} (in {{DefaultGroovyMethods}}):
{code:java}
public static String toCurrencyString(Number self)
public static String toCurrencyString(Number self, Locale locale)
public static String toPercentString(Number self)
public static String toPercentString(Number self, Locale locale)
{code}
*Parsing* — extension methods on {{CharSequence}} (in {{StringGroovyMethods}},
alongside {{toBigDecimal}}):
{code:java}
public static Number toNumber(CharSequence self, Locale locale)
// public static Number toCurrencyNumber(CharSequence self) // EDIT: removed
public static Number toCurrencyNumber(CharSequence self, Locale locale)
// public static Number toPercentNumber(CharSequence self) // EDIT: removed
public static Number toPercentNumber(CharSequence self, Locale locale)
{code}
All actually return {{BigDecimal}}.
h2. Example
{code:groovy}
import java.util.Locale
def de = Locale.GERMANY
assert 1234.5.toCurrencyString(de) == '1.234,50 €'
assert 0.945.toPercentString(de) == '94,5 %'
assert '1.234,50 €'.toCurrencyNumber(de) == 1234.50G // BigDecimal
assert '94,5 %'.toPercentNumber(de) == 0.945
assert '1.234,50'.toNumber(de) == 1234.5
{code}
h2. Tests
* Round-trip format→parse for representative locales (US, Germany, France,
Turkey — Turkey to cover leading {{%}} placement).
* Currency parse returns {{BigDecimal}}.
* Percent parse applies ÷100 scaling.
* Malformed input throws {{NumberFormatException}}.
> Add locale-aware number, currency and percent formatting/parsing to the GDK
> ---------------------------------------------------------------------------
>
> Key: GROOVY-12147
> URL: https://issues.apache.org/jira/browse/GROOVY-12147
> Project: Groovy
> Issue Type: Improvement
> Components: groovy-jdk
> Reporter: Paul King
> Assignee: Paul King
> Priority: Major
> Fix For: 6.0.0-beta-1
>
>
> h2. Summary
> EDIT: updated to match what it would be after PR#2699
> Groovy can already format and parse numbers, but only in a locale-invariant
> way. {{String.format(locale, ...)}} / {{sprintf}} cover locale-aware
> _grouped/decimal_ output, and {{"1234.50".toBigDecimal()}} covers _canonical_
> parsing — but there is no idiomatic way to:
> * format a number as *locale currency* ({{"€1.234,50"}}) or *locale percent*
> ({{"94,5 %"}}), or
> * parse a *locale-formatted* number, currency or percent string back to a
> {{Number}}.
> These are exactly the cases {{java.util.Formatter}} ({{%f}}, {{%%}})
> structurally cannot express (no currency conversion; {{%%}} is a literal
> sign, so no ÷100 scaling and wrong symbol placement in locales such as
> Turkish {{%94,5}} or German {{94,5 %}}). This issue adds a small set of GDK
> extension methods that wrap {{java.text.NumberFormat}} to close that gap.
> h2. Motivation
> * {{StringGroovyMethods}} conversions ({{toInteger}}, {{toDouble}},
> {{toBigDecimal}}, {{isNumber}}, …) are locale-invariant by construction
> ({{new BigDecimal(str)}} / {{Double.valueOf(str)}}) — they reject grouping
> separators, foreign decimal marks, currency symbols and percent signs.
> * {{sprintf}}/{{Formatter}} handles grouped/decimal numbers and localized
> {{%t}} dates, so no new date/time or {{sprintf}} surface is needed.
> * The only genuinely missing capability is currency/percent formatting and
> locale-aware parsing — a handful of thin {{NumberFormat}} wrappers.
> h2. Proposed API
> *Formatting* — extension methods on {{Number}} (in {{DefaultGroovyMethods}}):
> {code:java}
> public static String toCurrencyString(Number self)
> public static String toCurrencyString(Number self, Locale locale)
> public static String toPercentString(Number self)
> public static String toPercentString(Number self, Locale locale)
> {code}
> *Parsing* — extension methods on {{CharSequence}} (in
> {{StringGroovyMethods}}, alongside {{toBigDecimal}}):
> {code:java}
> public static Number toNumber(CharSequence self, Locale locale)
> // public static Number toCurrencyNumber(CharSequence self) // EDIT: removed
> public static Number toCurrencyNumber(CharSequence self, Locale locale)
> // public static Number toPercentNumber(CharSequence self) // EDIT: removed
> public static Number toPercentNumber(CharSequence self, Locale locale)
> {code}
> All actually return {{BigDecimal}}.
> h3. Examples
> Formatting and parsing round-trip for a given locale:
> {code:groovy}
> assert 1234.5.toCurrencyString(Locale.US) == '$1,234.50'
> assert '$1,234.50'.toCurrencyNumber(Locale.US) == 1234.50G
> assert 0.5.toPercentString(Locale.US) == '50%'
> assert '50%'.toPercentNumber(Locale.US) == 0.5
> assert '1.234,5'.toNumber(Locale.GERMANY) == 1234.5
> {code}
> Locale-specific symbols and layout are honoured. Note that the JDK separates
> the
> number from the currency/percent symbol with a *non-breaking space* (U+00A0),
> so
> locale-formatted strings should not be hand-written with a plain space:
> {code:groovy}
> def de = Locale.GERMANY
> assert 1234.5.toCurrencyString(de) == "1.234,50\u00A0\u20AC"
> assert "1.234,50\u00A0\u20AC".toCurrencyNumber(de) == 1234.50G
> {code}
> h4. Contracts
> * *Parsing requires an explicit Locale.* There is no default-locale overload:
> 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 is a fair assumption, such as a
> console
> application reading what the user typed, pass {{Locale.getDefault()}}
> explicitly.
> * *Parsing is strict and exact.* The entire (trimmed) input must be consumed,
> so
> trailing junk is rejected rather than silently truncated, and the result is
> an
> exact {{BigDecimal}}:
> {code:groovy}
> import static groovy.test.GroovyAssert.shouldFail
> assert '50%'.toPercentNumber(Locale.US) instanceof BigDecimal
> shouldFail(NumberFormatException) { '$1,234.50 and
> more'.toCurrencyNumber(Locale.US) }
> {code}
> * Formatting rounds; parsing does not.* {{NumberFormat}}'s percent instance
> defaults to zero fraction digits, so the number-to-String direction is lossy:
> {code:groovy}
> assert 0.945.toPercentString(Locale.US) == '94%' // rounded, not '94.5%'
> assert '94.5%'.toPercentNumber(Locale.US) == 0.945 // parsing keeps the
> digits
> {code}
> For fraction digits, use {{NumberFormat.getPercentInstance(locale)}} with
> {{setMaximumFractionDigits(int)}} directly. For locale-aware formatting of a
> plain number, use {{String.format(locale, '%,.3f', n)}} — the GDK
> deliberately adds no {{toNumberString}}, since {{String.format}} already
> covers it.
> h2. Tests
> * Round-trip format→parse for representative locales (US, Germany, France,
> Turkey — Turkey to cover leading {{%}} placement).
> * Percent parse applies ÷100 scaling.
> * Malformed input throws {{NumberFormatException}}.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)