Paul King created GROOVY-12152:
----------------------------------

             Summary: 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


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. The guiding principle we landed on:
* {{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 work below applies that principle per method.

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

h4. B. Parsing / machine-ingestion

|| Method || Current default || Action ||
| {{CharSequence.toCurrencyNumber()}}, {{toPercentNumber()}} (no-arg) | 
{{Locale.getDefault()}} | Change default to {{ROOT}} (or drop the no-arg) 
*before 6.0.0*; keep the {{(Locale)}} variant; doc |
| {{CharSequence.toNumber(Locale)}} | {{Locale}}-only, no no-arg — _correct 
as-is_ | Leave {{Locale}}-only. The locale-independent parse is already 
provided by {{toBigDecimal()}}/{{toInteger()}}/{{as Number}} (Java-literal 
grammar, not ROOT). A no-arg would either duplicate those or introduce 
divergent {{NumberFormat}} semantics ({{"1,234"}} parses via ROOT 
{{NumberFormat}} but throws via {{toBigDecimal}}). |
| {{Date.parse(format, input)}} / +TimeZone (static) | default locale 
({{SimpleDateFormat}}) | Add {{(…, Locale)}} variant; doc |
| {{<java.time>.parse(text, pattern)}} ×9 types (static) | default locale 
({{ofPattern}}) | Add {{(…, Locale)}} variant; doc |

h4. C. Human-facing formatting defaulting to the JVM default locale

Defensible defaults, but each needs (1) a Javadoc line "uses the JVM's locale; 
pass a {{Locale}} for server/per-user output" and (2) a {{Locale}} variant 
where none exists.

|| Method || Locale variant today? || Action ||
| {{Number.toCurrencyString()}} / {{toPercentString()}} (no-arg) | yes | Doc 
the default; currency no-arg is a drop candidate (ROOT is meaningless for 
currency) |
| {{Date.format(String)}}, {{format(String, TimeZone)}}, 
{{Calendar.format(String)}} | no | Add {{(…, Locale)}} variant + doc |
| {{Date.getDateString()}} / {{getTimeString()}} / {{getDateTimeString()}} | no 
| Add a locale-aware variant (or doc as fixed default-locale) |
| {{<java.time>.format(String pattern)}} ×6 | no | Add {{(…, Locale)}} variant 
+ doc |
| {{<java.time>.format(FormatStyle)}} ×6 | no | Add {{(…, Locale)}} variant + 
doc |
| {{printf(...)}} ×4, {{sprintf(...)}} ×2 | no | Add a {{Locale}}-first 
overload (mirrors {{String.format(Locale, …)}}); doc |

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 precedent in groovy-json

{{groovy.json.JsonGenerator}} already models the pattern this issue recommends 
for machine-facing output:
{code:java}
protected static final String JSON_DATE_FORMAT        = 
"yyyy-MM-dd'T'HH:mm:ssZ";
protected static final Locale JSON_DATE_FORMAT_LOCALE = Locale.US;   // stable, 
not getDefault()
protected static final String DEFAULT_TIMEZONE        = "GMT";
// ...
public Options dateFormat(String format, Locale locale) { ... }      // 
explicit override
{code}
i.e. a stable default locale (not {{getDefault()}}), an explicit {{Locale}} 
override, and — being a config object — a *discoverable* default. Category C 
should converge on this shape.

h3. Acceptance / proposed 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