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