[ 
https://issues.apache.org/jira/browse/GROOVY-12152?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King updated GROOVY-12152:
-------------------------------
    Description: 
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. Or if the Locale variant explosion is not warranted, the 
JavaDoc should point to the JDK long-hand for that case.

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


  was:
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. Or if the Locale variant explosion is not warranted, the 
JavaDoc should point to the JDK long-hand for that case.

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



> 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. 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. Or if the Locale variant explosion is not 
> warranted, the JavaDoc should point to the JDK long-hand for that case.
> || 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 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