Skip to content

Commit e0bd662

Browse files
aphillipseemelimacchiaticatamorphismsffc
authored
Design document for percent formatting (#1068)
* Design document for percent formatting This document is focused for now on documenting the options. * Address comments * Update exploration/percent-format.md Co-authored-by: Eemeli Aro <[email protected]> * Update exploration/percent-format.md Co-authored-by: Eemeli Aro <[email protected]> * Update exploration/percent-format.md Co-authored-by: Eemeli Aro <[email protected]> * Update exploration/percent-format.md Co-authored-by: Eemeli Aro <[email protected]> * Address comments * Update exploration/percent-format.md Co-authored-by: Mark Davis <[email protected]> * Update exploration/percent-format.md * Update percent-format.md * Update percent-format.md * Add @macchiati's exact unit scaling example explanation ... well, with edits. * Update exploration/percent-format.md Co-authored-by: Eemeli Aro <[email protected]> * Apply suggestions from code review Co-authored-by: Eemeli Aro <[email protected]> * Update percent-format.md * Address 2025-05-05 call consensus In the 2025-05-05 call, we agreed to remove the proposed design so that we can commit the design document to the main line in the repo. * Update exploration/percent-format.md Co-authored-by: Eemeli Aro <[email protected]> * Update exploration/percent-format.md Co-authored-by: Eemeli Aro <[email protected]> * Apply suggestions from code review Co-authored-by: Eemeli Aro <[email protected]> * Address removal of `:math` * Address 2025-05-19 comments In our most recent call, we discussed adopting the proposed design found in this commit. Note the addition of selector behavior. * Update exploration/percent-format.md Co-authored-by: Tim Chevalier <[email protected]> * Update exploration/percent-format.md Co-authored-by: Tim Chevalier <[email protected]> * Move proposed solution to be one of the alternatives * Apply suggestions from code review Co-authored-by: Shane F. Carr <[email protected]> --------- Co-authored-by: Eemeli Aro <[email protected]> Co-authored-by: Mark Davis <[email protected]> Co-authored-by: Tim Chevalier <[email protected]> Co-authored-by: Eemeli Aro <[email protected]> Co-authored-by: Shane F. Carr <[email protected]>
1 parent f72b80a commit e0bd662

File tree

1 file changed

+394
-0
lines changed

1 file changed

+394
-0
lines changed

exploration/percent-format.md

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
# Formatting Percent Values
2+
3+
Status: **Proposed**
4+
5+
<details>
6+
<summary>Metadata</summary>
7+
<dl>
8+
<dt>Contributors</dt>
9+
<dd>@aphillips</dd>
10+
<dd>@eemeli</dd>
11+
<dt>First proposed</dt>
12+
<dd>2025-04-07</dd>
13+
<dt>Pull Requests</dt>
14+
<dd>#1068</dd>
15+
</dl>
16+
</details>
17+
18+
## Objective
19+
20+
_What is this proposal trying to achieve?_
21+
22+
One the capabilities present in ICU MessageFormat is the ability to format a number as a percentage.
23+
This design enumerates the approaches considered for adding this ability as a _default function_
24+
in Unicode MessageFormat.
25+
26+
## Background
27+
28+
_What context is helpful to understand this proposal?_
29+
30+
> [!NOTE]
31+
> This design is an outgrowth of discussions in #956 and various teleconferences.
32+
33+
Developers and translators often need to insert a numeric value into a formatted message as a percentage.
34+
The format of a percentage can vary by locale including
35+
the symbol used,
36+
the presence or absence of spaces,
37+
the shaping of digits,
38+
the position of the symbol,
39+
and other variations.
40+
41+
One of the key problems is whether the value should be "scaled".
42+
That is, does the value `0.5` format as `50%` or `0.5%`?
43+
Developers need to know which behavior will occur so that they can adjust the value passed appropriately.
44+
45+
> [!NOTE]
46+
> In ICU4J:
47+
> - MessageFormat (MF1) scales.
48+
> - MeasureFormat does not scale.
49+
>
50+
> In JavaScript:
51+
> - `Intl.NumberFormat(locale, { style: 'percent' })` scales
52+
> - `Intl.NumberFormat(locale, { style: 'unit', unit: 'percent' })` does not scale
53+
54+
It is also possible for Unicode MessageFormat to provide support for scaling in the message itself.
55+
Since we've removed the `:math` function (at least for now), this would have to be through either
56+
the re-introduction of `:math` or through a specialized scaling function.
57+
58+
An addition concern is whether to add a dedicated `:percent` function,
59+
use one of the existing number-formatting functions `:number` and `:integer` with an option `type=percent`,
60+
or use the proposed _optional_ function `:unit` with an option `unit=percent`.
61+
Combinations of these approached might also be used.
62+
63+
### Unit Scaling
64+
65+
This section describes the scaling behavior of ICU's `NumberFormatter` class and its `unit()` method,
66+
which is one model for how Unicode MessageFormat might implement percents and units.
67+
There is a difference between _input_ scaling and _output_ scaling in ICU's `NumberFormatter`.
68+
69+
For example, an input of <3.5, `meter`> with `meter` as the output unit doesn't scale.
70+
71+
If one supplies <0.35 `percent`> as the input and the output unit were `percent`,
72+
`MeasureFormat` would format as 0.35%.
73+
Just like `meter` ==> `meter` doesn't scale.
74+
75+
However, if one supplies a different input unit, then percent does scale
76+
(just like `meter` ==> `foot`).
77+
The base unit for such dimensionless units is called 'part'.
78+
In MF, a bare number literal, such as `.local $foo = {35}`
79+
or an implementation-specific number type (such as an `int` in Java)
80+
might be considered to use the input unit of `part`
81+
unless we specified that the `percent` unit value or `:percent` function overrode the `part` unit with `percent`.
82+
83+
With <0.35 `part`> as the input and the output unit of `percent`, the format is "35%".
84+
85+
| Amount | Input Unit | Formatted Value with... | Unit |
86+
|---|---|---|---|
87+
| 0.35 | part | 0.35 | part |
88+
| 0.35 | part | 35.0 | percent |
89+
| 0.35 | part | 350.0 | permille |
90+
| 0.35 | part | 3500.0 | permyriad |
91+
| 0.35 | part | 350000.0 | part-per-1e6 |
92+
| 0.35 | part | 3.5E8 | part-per-1e9 |
93+
94+
## Use-Cases
95+
96+
_What use-cases do we see? Ideally, quote concrete examples._
97+
98+
Developers wish to write messages that format a numeric value as a percentage in a locale-sensitive manner.
99+
100+
The numeric value of the operand is not pre-scaled because it is the result of a computation,
101+
e.g. `var savings = discount / price`.
102+
103+
The numeric value of the operant is pre-scaled,
104+
e.g. `var savingsPercent = 50`
105+
106+
Users need control over most formatting details, identical to general number formatting:
107+
- negative number sign display
108+
- digit shaping
109+
- minimum number of fractional digits
110+
- maximum number of fractional digits
111+
- minimum number of decimal digits
112+
- group used (for very large percentages, i.e. > 999%)
113+
- etc.
114+
115+
## Requirements
116+
117+
_What properties does the solution have to manifest to enable the use-cases above?_
118+
119+
- **Be consistent**
120+
- Any solution for scaling percentages should be a model for other, similar scaling operations,
121+
such as _per-mille_ or _per-myriad_,
122+
as well as other, non-percent or even non-unit scaling.
123+
This does not mean that a scaling mechanism or any particular scaling mechanism itself is a requirement.
124+
- Any solution for formatting percentages should be a model for solving related problems with:
125+
- per-mille
126+
- per-myriad
127+
- compact notation
128+
- scientific notation
129+
- (others??)
130+
131+
## Constraints
132+
133+
_What prior decisions and existing conditions limit the possible design?_
134+
135+
## Proposed Design
136+
137+
_Describe the proposed solution. Consider syntax, formatting, errors, registry, tooling, interchange._
138+
139+
TBD
140+
141+
## Alternatives Considered
142+
143+
_What other solutions are available?_
144+
_How do they compare against the requirements?_
145+
_What other properties they have?_
146+
147+
### Combinations of Functions and Scaling
148+
149+
Any proposed design needs to choose one or more functions
150+
each of which has a scaling approach
151+
or a combination of both.
152+
It is possible to have separate functions, one that is scaling and one that is non-scaling.
153+
154+
Some working group members suspect that having a function that scales and one that does not
155+
would represent a hazard,
156+
since users would be forced to look up which one has which behavior.
157+
158+
Other working group members have expressed that the use cases for pre-scaled vs. non-pre-scaled are separate
159+
and that having separate functions for these is logically sensible.
160+
161+
### Function Alternatives
162+
163+
#### Use `:unit`
164+
165+
Leverage the `:unit` function by using the existing unit option value `percent`.
166+
The ICU implementation of `MeasureFormat` does **_not_** scale the percentage,
167+
although this does not have to be the default behavior of UMF's percent unit format.
168+
169+
```
170+
You saved {$savings :unit unit=percent} on your order today!
171+
```
172+
173+
The `:unit` alternative could also support other unit-like alternatives, such as
174+
_per-mille_ and _per-myriad_ formatting.
175+
It doesn't fit as cleanly with other notational variations left out of v47, such as
176+
compact notation (1000000 => 1M, 1000 => 1K),
177+
or scientific notation (1000000 => 1.0e6).
178+
179+
_Pros_
180+
- Uses an existing set of functionality
181+
- Might provide a more consistent interface for formatting "number-like" values
182+
- Keeps percentage formatting out of `:number` and `:integer`, limiting the scope of those functions
183+
184+
_Cons_
185+
- `:unit` won't be REQUIRED, so percentage format will not be guaranteed across implementations.
186+
Requiring `:unit unit=percent` would be complicated at best.
187+
- Implementation of `:unit` in its entirely requires significantly more data than implementation of
188+
percentage formatting.
189+
- More verbose placeholder
190+
191+
---
192+
193+
#### Use `:number`/`:integer` with `style=percent`
194+
195+
Use the existing functions for number formatting with a separate `style` option for `percent`.
196+
(This was previously the design)
197+
198+
```
199+
You saved {$savings :number style=percent} on your order today!
200+
```
201+
202+
_Pros_
203+
- Consistent with ICU MessageFormat
204+
205+
_Cons_
206+
- It's the only special case remaining in these functions,
207+
unless we also restore compact, scientific, and other notational variations.
208+
209+
---
210+
211+
#### Use a dedicated `:percent` function
212+
213+
Use a new function `:percent` dedicated to percentages.
214+
215+
```
216+
You saved {$savings :percent} on your order today!
217+
```
218+
219+
> [!NOTE]
220+
> @sffc suggested that we should consider other names for `:percent`.
221+
> The name shown here could be considered a placeholder pending other suggestions.
222+
223+
_Pros_
224+
- Least verbose placeholder
225+
- Clear what the placeholder does; self-documenting
226+
- Consistent with separating specialized formats from `:number`/`:integer`
227+
as was done with `:currency`
228+
- Makes it possible to apply a `scaling` option to only percent formatting.
229+
230+
_Cons_
231+
- Adds to a (growing) list of functions
232+
- Not "special enough" to warrant its own formatter?
233+
- Adds yet another numeric function, with its own subset of numeric function options.
234+
235+
---
236+
237+
#### Use a generic scaling function
238+
239+
Use a new function with a more generic name so that it can be used to format other scaled values.
240+
For example, it might use an option `unit` to select `percent`/`permille`/etc.
241+
242+
```
243+
You saved {$savings :dimensionless unit=percent} on your order today!
244+
You saved {$savings :scaled per=100} on your order today!
245+
```
246+
247+
_Pros_
248+
- Could be used to support non-percent/non-permille scales that might exist in other cultures
249+
- Somewhat generic
250+
- Unlike currency or unit values, "per" units do not have to be stored with the value to prevent loss of fidelity,
251+
since the scaling is done to a plain old number.
252+
This would not apply if the values are not scaled.
253+
254+
_Cons_
255+
- Only percent and permille are backed with CLDR data and symbols.
256+
Other scales would impose an implementation burden.
257+
- More verbose. Might be harder for users to understand and use.
258+
259+
### Scaling Alternatives
260+
261+
#### No Scaling
262+
User has to scale the number.
263+
The value `0.5` formats as `0.5%`
264+
265+
> Example.
266+
> ```
267+
> .local $pctSaved = {50}
268+
> {{{$pctSaved :percent}}}
269+
> ```
270+
> Prints as `50%`.
271+
272+
#### Always Scale
273+
Implementation always scales the number.
274+
The value `0.5` formats as `50%`
275+
276+
> Example.
277+
> ```
278+
> .local $pctSaved = {50}
279+
> {{{$pctSaved :percent}}}
280+
> ```
281+
> Prints as `5,000%`.
282+
283+
#### Optional Scaling
284+
Function automatically does (or does not) scale,
285+
but there is an option to switch to the non-default behavior.
286+
Such an option might be:
287+
- An option with a name like `scaling` with boolean-like values `true` and `false`
288+
- An option with a name like `scale` with
289+
digit size option values
290+
limited to a small set of supported values (possibly only `1` and `100`)
291+
292+
> Example. Note that `scale=false` is only to demonstrate switching.
293+
>```
294+
> .local $pctSaved = {50}
295+
> {{{$pctSaved :percent} {$pctSaved :percent scale=false}}}
296+
>```
297+
> Prints as `5,000% 50%` if `:percent` is autoscaling by default
298+
299+
#### Provide scaling via a function
300+
Regardless of the scaling done by the percent formatting function,
301+
there might need to be an in-message mechanism for scaling/descaling values.
302+
The function `:math` was originally proposed to support offsets in number matching/formatting,
303+
although the WG removed this proposal and replaced with with an `:offset` function in May 2025.
304+
Reintroducing `:math` to support scaling
305+
or the proposal of a new function dedicated to scaling might address the need for value adjustment.
306+
307+
> Example using `:math` as a hypothetical function name
308+
>```
309+
> .local $pctSaved = {0.5}
310+
> .local $pctScaled = {$pctSaved :math exp=2}
311+
> {{{$pctSaved :percent} {$pctScaled :unit unit=percent}}}
312+
>```
313+
> Prints as `50% 50%` if `:percent` is autoscaling by default and `:unit` is not.
314+
315+
_Pros_
316+
- Users may find utility in performing math transforms in messages rather than in business logic.
317+
- Should be easy to implement, given that basic math functionality is common
318+
319+
_Cons_
320+
- Implementation burden, especially when providing generic mathematical operations
321+
- Designs should be generic and extensible, not tied to short term needs of a given formatter.
322+
- Potential for abuse and misuse is higher.
323+
- "Real" math utilities or classes tend to have a long list of functions with many capabilities.
324+
A complete implementation would require a lot of design work and effort or introduce
325+
instability into the message regime as new options are introduced over time.
326+
Compare with `java.lang.Math`
327+
328+
Two proposals exist for `:math`-like scaling:
329+
330+
##### Use `:math exp` (`:exp`??) to scale
331+
Provide functionality to scale numbers with integer powers of 10 using the `:math` function.
332+
333+
Examples using `:unit`, each of which would format as "Completion: 50%.":
334+
```
335+
.local $n = {50}
336+
{{Completion: {$n :unit unit=percent}.}}
337+
338+
.local $n = {0.5 :math exp=2}
339+
{{Completion: {$n :unit unit=percent}.}}
340+
```
341+
342+
_Pros_
343+
- Avoids multiplication of random values
344+
- Useful for other scaling operations
345+
346+
_Cons_
347+
- Cannot use _digit size option_ as the `exp` option value type, since negative exponents are a Thing
348+
349+
350+
##### Use `:math multiply` (`:multiply`??) to scale
351+
Provide arbitrary integer multiplication functionality using the `:math` function.
352+
353+
Examples using `:unit`, each of which would format as "Completion: 50%.":
354+
```
355+
.local $n = {50}
356+
{{Completion: {$n :unit unit=percent}.}}
357+
358+
.local $n = {0.5 :math multiply=100}
359+
{{Completion: {$n :unit unit=percent}.}}
360+
```
361+
362+
_Pros_
363+
- Can be used for other general purpose math
364+
365+
_Cons_
366+
- Increases implementation burden: multiplication must be handled on arbitrary numeric input types
367+
368+
---
369+
370+
### Why not both?
371+
372+
Rather than choosing only one option, choose multiple parallel solutions:
373+
374+
- REQUIRE the `:unit` function for all implementations
375+
- Only specific `unit` option values are required, initially the unit `percent`.
376+
- The function `:unit unit=percent` does not scale the operand, e.g. `{5 :unit unit=percent}` formats as `5%`.
377+
- REQUIRE the `:number` function to support `style=percent` as an option
378+
- The function `:number`scales the operand, e.g. `{5 :number style=percent}` formats as `500%`.
379+
Note that the selector selects on the scaled value
380+
(selectors currently cannot select fractional parts)
381+
382+
> Examples. These are equivalent **except** that `:unit` does NOT scale.
383+
>```
384+
> {{You have {$pct :number style=percent} remaining.}}
385+
> {{You have {$scaledPct :unit unit=percent} remaining.}}
386+
>```
387+
> Selector example:
388+
>```
389+
> .local $pct = {0.05 :number style=percent}
390+
> .match $pct
391+
> 5 {{This pattern is selected}}
392+
> one {{You have {$pct} left.}}
393+
> * {{You have {$pct} left.}}
394+
>```

0 commit comments

Comments
 (0)