[ 
https://issues.apache.org/jira/browse/GROOVY-12152?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18095836#comment-18095836
 ] 

Jochen Theodorou edited comment on GROOVY-12152 at 7/13/26 9:33 AM:
--------------------------------------------------------------------

assert 0.945.toPercentString(Locale.US) == '94%' 

why is that not 95%?


was (Author: blackdrag):
{{assert 0.945.toPercentString(Locale.US) == '94%' }}

why is that not 95%?

> Clarify Locale handling throughout the GDK
> ------------------------------------------
>
>                 Key: GROOVY-12152
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12152
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Priority: Major
>
> h3. Summary
> Assess and improve how the GDK handles {{java.util.Locale}}, following the 
> discussion on the GROOVY-12147 dev thread. The goal is *not* to remove 
> convenience methods, but to make each locale-sensitive method's contract 
> explicit, offer an explicit-{{Locale}} variant where one is missing, and fix 
> a couple of parse defaults before 6.0.0 ships. Almost all of this is 
> additive/non-breaking.
> h3. Background
> Two recent efforts touched this area:
> * GROOVY-12147 added locale-aware number/currency/percent formatting and 
> parsing.
> * GROOVY-12055 moved identifier/token case-folding to {{Locale.ROOT}}.
> On the GROOVY-12147 thread it was noted that methods depending on the JVM 
> {{default}} locale are hard to test and surprising on servers/multi-locale 
> processes. A guiding principle we might want to land on (to match the JDK 
> stance):
> * {{Locale.ROOT}} — data meant for machines: protocols, DB, file formats, 
> anything persisted or re-parsed.
> * {{Locale.getDefault()}} — output meant for the human running the code (only 
> a valid proxy for "the user" in single-user/client contexts).
> The catch is that {{Locale.getDefault()}} is only a good proxy for "the 
> human" when there is one human and they _are_ the process:
> * On the *command line, a script, or a desktop app*, the JVM default 
> genuinely reflects the person reading the output — the default-locale 
> convenience is exactly right.
> * On a *web server, batch job, or any multi-tenant process*, the JVM default 
> is _not_ the end user's locale — it is the container's ({{LANG}}, whatever 
> ops set, usually en-US or C/POSIX). The actual user is determined *per 
> request* (Accept-Language, a profile setting) and arrives as data, not as the 
> process default. There, a default-locale method silently formats for the 
> wrong human, and it looks correct in dev only because the dev box default 
> happens to match the test expectations.
> This is also why default-locale methods are hard to test meaningfully (you 
> have to pin a locale, which defeats the "default"), and why the convenience 
> must always be paired with an explicit-{{Locale}} variant: the same call has 
> a different "correct" answer depending on where it runs, and only the caller 
> knows which deployment it is in.
> The items below are potential work we might want to do to get closer to the 
> above guiding principle.
> h3. Scope
> All actionable items are in GDK extension classes ({{StringGroovyMethods}}, 
> {{DefaultGroovyMethods}}, 
> {{DateUtilExtensions}}/{{DateUtilStaticExtensions}}, 
> {{DateTimeExtensions}}/{{DateTimeStaticExtensions}}).
> h4. A. Case-insensitive / case-folding on human text (consistency + docs)
> These take arbitrary user text, fold case, and are silent about their 
> (locale-independent) contract.
> || Method || Current || Action ||
> | {{containsIgnoreCase}} | {{toLowerCase(Locale.ROOT)}} both sides | Align 
> mechanism with starts/ends; Javadoc "locale-independent" |
> | {{startsWithIgnoreCase}}, {{endsWithIgnoreCase}} | JDK {{equalsIgnoreCase}} 
> (char-wise) | Same doc note; pick one shared mechanism |
> | {{capitalize}}, {{uncapitalize}} | {{Character.to{Upper,Lower}Case}} 
> (single char) | Javadoc note only; low priority |
> A {{Locale}} variant is generally *not* warranted here (search/compare wants 
> stability).
> h3. Category B — Parsing
> *Decision (implemented in GROOVY-12147):* locale-sensitive parsing methods 
> take a
> *mandatory* {{Locale}}; no default-locale overload is provided.
> This supersedes the earlier proposal to default {{toCurrencyNumber()}} and
> {{toPercentNumber()}} to {{Locale.ROOT}}. That is not viable: 
> {{Locale.ROOT}}'s
> currency symbol is the placeholder {{\u00A4}}, so 
> {{'$1,234.50'.toCurrencyNumber()}}
> under {{ROOT}} would throw rather than parse. There is no meaningful 
> locale-free
> reading of a currency or percent string.
> Nor is {{Locale.getDefault()}} simply wrong for parsing — a console 
> application
> reading what the user typed is a fair use of it. The point is narrower: parsed
> 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. 
> The
> default is therefore right often enough to be tempting and rarely enough to 
> be a
> trap, which reduces the value of a defaulted overload below the cost of the 
> silent
> failures it invites. Callers who do want it write {{Locale.getDefault()}}
> explicitly — one argument, and the assumption is then visible at the call 
> site.
> *Remaining work:* document that the existing JDK-backed parsers
> ({{toInteger}}, {{toLong}}, {{toBigDecimal}}, {{toBigInteger}}, {{isNumber}}) 
> are
> locale-*independent* — always {{.}} as the decimal separator, no grouping — 
> and
> cross-link them to the locale-aware variants added in GROOVY-12147, so the 
> choice
> between the two families is discoverable from either side.
> h3. Category C — Formatting
> *Decision (implemented in GROOVY-12147):* locale-sensitive formatting methods
> provide both an explicit-{{Locale}} variant and a default-locale convenience
> overload. Unlike parsing, the default locale is usually the right choice here:
> formatting produces human-facing output, and in a single-user context (CLI,
> desktop) the JVM default *is* the user's locale. The convenience overloads
> document that they use {{Locale.getDefault()}}, and note that in a server or 
> other
> multi-tenant context the default is the container's locale, not the end 
> user's.
> *Correction:* {{NumberFormat}}'s percent instance defaults to zero fraction
> digits, so percent formatting rounds:
> {code:groovy}
> assert 0.945.toPercentString(Locale.US) == '94%'   // not '94.5%'
> {code}
> The javadoc has been fixed accordingly. Formatting rounds and parsing does 
> not, so
> the {{toXxxString}}/{{toXxxNumber}} pairs do not round-trip in the
> Number -> String -> Number direction.
> *Decision:* no {{toNumberString(Locale)}} is added. {{String.format(locale, 
> '%,.3f', n)}}
> already covers locale-aware formatting of a plain number, and a 
> {{toNumberString}}
> would imply a round-trip guarantee with {{toNumber}} that {{NumberFormat}} 
> does not
> give. Currency and percent are different — {{java.util.Formatter}} has no 
> conversion
> for either — which is why those methods earn their place.
> *Remaining work:* {{sprintf}} and {{printf}} (DefaultGroovyMethods, {{@since 
> 1.0}} /
> {{@since 1.5.0}}) delegate to {{PrintStream.printf}} and so silently use
> {{Locale.getDefault()}}, with *no* explicit-{{Locale}} variant anywhere in 
> the GDK —
> users must drop out to {{String.format(Locale, ...)}} to control it. This is 
> now
> documented on all six public overloads (GROOVY-12147). Adding {{Locale}} 
> overloads,
> e.g. {{sprintf(Object self, Locale locale, String format, Object[] values)}}, 
> is a
> candidate sub-task; it would cover the plain-number formatting case as a side
> effect.
> h4. Cross-cutting: Javadoc {{@see}} routing
> Wire bidirectional {{@see}} links so each method routes the reader to the 
> right sibling: the locale-aware variant, the locale-free variant, and the raw 
> JDK API ({{NumberFormat.getNumberInstance(Locale)}}, 
> {{DateTimeFormatter.ofPattern(pattern, locale)}}, {{new 
> SimpleDateFormat(pattern, locale)}}). This makes the "no no-arg is 
> intentional" decisions self-documenting and keeps the JDK escape hatch one 
> hop away.
> h3. Out of scope
> The GROOVY-12055 identifier/token case-folding (class/property/CLI/SQL names, 
> etc., mostly in non-extension classes) stays on {{Locale.ROOT}} — the input 
> domain is machine tokens, so ROOT is correct and no doc change is warranted.
> h3. Note: existing precedents for machine-facing locale handling
> Two existing methods already model what this issue recommends for 
> machine-facing output — a stable pinned locale rather than {{getDefault()}}:
> * {{DateUtilStaticExtensions.parseToStringDate(Date, String)}} — a *GDK 
> extension method* that hard-bakes {{Locale.US}} because it parses the fixed 
> {{Date.toString()}} format; {{getDefault()}} would break under a non-US 
> locale. (No override needed — the format is fixed.)
> * {{groovy.json.JsonGenerator}} — a config object with a stable default 
> ({{Locale.US}}, ISO-ish pattern, GMT) *and* an explicit {{Locale}} override:
> {code:java}
> protected static final Locale JSON_DATE_FORMAT_LOCALE = Locale.US;   // 
> stable, not getDefault()
> public Options dateFormat(String format, Locale locale) { ... }      // 
> explicit override
> {code}
> Together they bracket the pattern: machine/ingestion pins a stable locale 
> (Category B), and where an override is useful it is offered explicitly 
> (Category C). The precedent for both already exists in the codebase — 
> including inside the extension-method surface.
> h3. Potential policy
> The rule is not "no default-locale methods" — it is "no method depends on the 
> default locale _silently or without an escape hatch_". Concretely, a new 
> locale-sensitive GDK method is acceptable when:
> * It offers an explicit-{{Locale}} variant (or the non-locale form is 
> documented as living elsewhere, e.g. {{toBigDecimal()}} for locale-free 
> number parsing).
> * Its no-arg/default behaviour is stated in the Javadoc — which locale it 
> uses and why.
> * The default is chosen by audience, not by habit:
> ** _Human-facing formatting_ (currency/percent/date-for-display, {{printf}}) 
> *may* default to {{Locale.getDefault()}}, provided the {{Locale}} variant 
> exists and the default is documented ("uses the JVM's locale; pass a 
> {{Locale}} for server/per-user output").
> ** _Machine-facing / ingestion_ (parsing, serialization, anything persisted 
> or re-parsed) defaults to {{Locale.ROOT}} or requires an explicit {{Locale}} 
> — never {{getDefault()}}.
> Additionally:
> * Avoid new locale-sensitive method ships _default-locale-only_ (default 
> behaviour with neither a {{Locale}} variant nor a documented contract), but 
> if unavoidable, explicit Javadoc should make the default explicit and point 
> to the JDK long-hand to get Locale behavior.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to