Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DESIGN] Number selection design refinements #859

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

aphillips
Copy link
Member

This is to build up and capture technical considerations for how to address the issues raised by @eemeli's PR #842.

This is to build up and capture technical considerations for how to address the issues raised by @eemeli's PR #842.
@aphillips aphillips added design Design principles, decisions formatting LDML46 LDML46 Release (Tech Preview - October 2024) labels Aug 13, 2024
@aphillips aphillips requested a review from eemeli August 13, 2024 16:36
Comment on lines 93 to 103
As a user, I want the selector to match the options specified:
```
.local $num = {123.456 :number maximumSignificantDigits=2 maximumFractionDigits=2 minimumFractionDigits=2}
.match {$num}
120.00 {{This matches}}
120 {{This does not match}}
123.47 {{This does not match}}
123.456 {{This does not match}}
1.2E2 {{Does this match?}}
* {{ ... }}
```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an accidentally good example of the difficulty of defining number selection that accounts for formatting options:

const nf = new Intl.NumberFormat('en', {
  maximumSignificantDigits: 2,
  maximumFractionDigits: 2,
  minimumFractionDigits: 2
})
nf.format(123.456) === '120'

In other words, at least in JavaScript it would not make sense for the 120.00 variant to be selected, because that doesn't match how $num would be formatted.

I think that defining the combined behaviour of specific number formatting options is, and should be kept, outside the scope of MF2. It is not our place to define how number formatting happens, or how the :number options are to be interpreted.

As far as I can tell, this leaves us with two realistic options:

  1. Declare exact value matching to be implementation-specific.
  2. Define exact value matching to not depend on formatting options.

I would prefer option 2 (hence my earlier PR), but I'd be fine with option 1 as well (effectively, what the spec currently does). I would not be fine with the excessive complexity required of the spec and implementations if we were to require matching to account for the formatting options in a predefined manner.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the matching behavior does not have to match the formatting behavior.

The JS result looks like a bug to me? Maybe it isn't actually a bug, but it looks like one, given how much effort was expended trying to get two fraction digits. The following produces $120, which seems... odd:

const nf = new Intl.NumberFormat('en', {
  maximumSignificantDigits: 2,
  maximumFractionDigits: 2,
  minimumFractionDigits: 2,
  style: 'currency',
  currency: 'USD',
  currencyDisplay: 'symbol'
});
console.log(nf.format(123.456));

The existence of this feature/bug does not mean that MF2's definition of numeric matching has to be the same as the formatting output. It might be inconvenient for the implementer in JS, but that doesn't make it the wrong choice.

Do you disagree with the logic behind the example?

Do you think that multiple keys should match in the example? Is there an example where 123.456 (i.e. exact numeric value) is the better match?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the matching behavior does not have to match the formatting behavior.

Really? You'd be fine with 120.00 but not 120 working as the key for a variant where the number would get formatted as 120?

The JS result looks like a bug to me? Maybe it isn't actually a bug, but it looks like one, given how much effort was expended trying to get two fraction digits.

It's not a bug; this result falls out of the combination of fraction and significant digits depending on the value of roundingPriority, which by default prefers significant digit options.

It might be inconvenient for the implementer in JS, but that doesn't make it the wrong choice.

While the implementation complexity does matter, I'm more concerned about the expectations of developers. In the very rare cases where a selection on an exact match on a value like 120 matters, I cannot believe that it makes sense to require a developer to be aware of the specific MF2 selection (but not formatting!) understanding of how the formatting options combine.

Do you disagree with the logic behind the example?

Yes. I do not think it's right for us to enforce a specific meaning for number formatting options in MF2, which is required to say that 120.00 would be selected, but 120 would not be.

Do you think that multiple keys should match in the example? Is there an example where 123.456 (i.e. exact numeric value) is the better match?

With this example, if we want to explicitly define which variant is selected, then I think 123.456 is the only available choice even if it's suboptimal. If we're not okay with that, then I think the next-best option is to leave it up to implementations to do exact matching, and then any of these variants could end up getting selected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really? You'd be fine with 120.00 but not 120 working as the key for a variant where the number would get formatted as 120?

I expect that the formatting behavior will be locale affected while the selection behavior won't be. What's important is that the message author can look at the message (and only the message) and know how to specify the key for a given exact value. That knowledge, for default registry functions, should be transferable to any message/implementation.

It's not a bug; this result falls out of the combination of fraction and significant digits depending on the value of roundingPriority, which by default prefers significant digit options.

Okay. (I think it is weird, but see that it flows from the implementation of ICU's newer number formatter, which bundles everything up inside of "precision"). Since I think that using significant digits to perform number "trimming" (as I do in the example) is probably undesirable vs. actually mutating the value passed in, we should probably focus on the other details.

Yes. I do not think it's right for us to enforce a specific meaning for number formatting options in MF2, which is required to say that 120.00 would be selected, but 120 would not be.

Because of the significant digits stuff? Or because you don't think the fraction digits should have any effect? I'm fine with saying 120 would be selected, if we decide to follow the Intl.NumberFormat/ICU behavior. If we did that, then the example would be:

.local $num = {123.456 :number maximumSignificantDigits=2 maximumFractionDigits=2 minimumFractionDigits=2}
.match {$num}
120.00  {{This does not match}}
120     {{This matches because significant digits wins}}
123.47  {{This does not match}}
123.456 {{This does not match}}
1.2E2   {{Does this match?}}
*       {{ ... }}

Although this is a better example:

.local $num = {123.456 :number maximumFractionDigits=2 minimumFractionDigits=2}
.match {$num}
123.46  {{This matches}}
123.45  {{This does not match because rounding}}
123     {{This does not match}}
123.456 {{This does not match}}
1.23456E2   {{Does this match?}}
*       {{ ... }}

With this example, if we want to explicitly define which variant is selected, then I think 123.456 is the only available choice even if it's suboptimal. If we're not okay with that, then I think the next-best option is to leave it up to implementations to do exact matching, and then any of these variants could end up getting selected.

I don't agree because it is suboptimal and because I'd have a hard time explaining it. For example, currency conversions often have many decimal places, but I don't want to show the extra precision available. Or I might want to turn the value's fractional part off for display. I shouldn't have to match 123.456 if the display is 123 I think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. It should match because it needs to match the visible digits (whatever the numberSystem / decimal/grouping- separators.

And it also goes for the integer case, as below

.local $num = {456 :number maximumSignificantDigits=2 minimumSignificantDigits=2}
.match {$num}
460  {{This matches}}
450  {{This does not match}}
456  {{This does not match}}
456.0  {{This does not match}}
*       {{ ... }}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect that the formatting behavior will be locale affected while the selection behavior won't be. What's important is that the message author can look at the message (and only the message) and know how to specify the key for a given exact value. That knowledge, for default registry functions, should be transferable to any message/implementation.

I agree with this. But I think we find ourselves on different paths starting from the same premise. One consideration that I include in my thinking is that we cannot assume that MF2 authors will remember the details of how formatting options interact with exact variant keys during selection, if and when that behaviour differs from what happens during formatting. And different implementations' formatters will differ in their interpretations of options baskets.

Since I think that using significant digits to perform number "trimming" (as I do in the example) is probably undesirable vs. actually mutating the value passed in, we should probably focus on the other details.

Do we have an example message using non-integer number selection that we could be considering? I continue to be concerned that we're trying to add excessive complexity to solve for an extremely rare edge case .

Yes. I do not think it's right for us to enforce a specific meaning for number formatting options in MF2, which is required to say that 120.00 would be selected, but 120 would not be.

Because of the significant digits stuff? Or because you don't think the fraction digits should have any effect?

Because defining exactly which of these matches (as opposed to 123.456) requires defining a selection-specific meaning for these options that would be different from their meaning during formatting, and that this is so obscure no-one will remember it, or be able to predict it when writing a message.

.local $num = {123.456 :number maximumFractionDigits=2 minimumFractionDigits=2}
.match {$num}
123.46  {{This matches}}
123.45  {{This does not match because rounding}}
123     {{This does not match}}
123.456 {{This does not match}}
1.23456E2   {{Does this match?}}
*       {{ ... }}

If $num were defined as an input, we could not guarantee that it would be formatted as 123.46. For example, in JS it could carry with it a formatting option { roundingMode: 'floor' }, which would mean that it'd get formatted as 123.45.

Trying to support some formatting options during selection will not produce predictable results. Trying to support all formatting options during selection is impossible, because not all formatters work the same way. Supporting no formatting options during selection is predictable, and uniform across all implementations. Yes, it does require mutating the input value in some edge cases, but those cases are made predictable.

I don't agree because it is suboptimal and because I'd have a hard time explaining it. For example, currency conversions often have many decimal places, but I don't want to show the extra precision available. Or I might want to turn the value's fractional part off for display. I shouldn't have to match 123.456 if the display is 123 I think.

If we want to ensure that the selected key and the formatted value match, then we must leave exact selection up to each implementation, because different implementations will not always agree in how they format a given value and its bag of options.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we cannot assume that MF2 authors will remember the details of how formatting options interact with exact variant keys during selection, if and when that behaviour differs from what happens during formatting

I disagree, from the perspective that they already kind of do. The behavior difference isn't that great and is mostly confined to locale-based formatting details (grouping, shaping, separator characters, etc.). Most of these folks are technical enough to know that -123.45 refers to a negative number with a specific value. Is it really that unintuitive what that key means?

If $num were defined as an input, we could not guarantee that it would be formatted as 123.46. For example, in JS it could carry with it a formatting option { roundingMode: 'floor' }, which would mean that it'd get formatted as 123.45.

How could an input variable have a formatting option pre-attached (roundingMode is an Intl.NumberFormat option, no?)? The formatting options are part of the formatter or some expression visible in the message.

Note that the keys match use cases that the message author is trying to fulfill. Exact match keys need to "exactly" match the perceived value, which, as I think this thread shows, is influenced by formatting options.

Trying to support some formatting options during selection will not produce predictable results. Trying to support all formatting options during selection is impossible, because not all formatters work the same way. Supporting no formatting options during selection is predictable, and uniform across all implementations. Yes, it does require mutating the input value in some edge cases, but those cases are made predictable.

This goes back to the transitivity discussion (still pending in the group). I think it's clear that some options are transitive and some not when it comes to formatting. I think the same can be said of selectors.

I think that the behavior of a given selector is defined by that selector. We are only talking here about the number selectors in the default registry. For sure other functions might work differently.

Do we have an example message using non-integer number selection that we could be considering? I continue to be concerned that we're trying to add excessive complexity to solve for an extremely rare edge case .

Fractional selection is probably most common when working with currencies, percents, and unit values. I can write/unearth some examples later.

In the meantime... I'm fearful that we're not capturing this good discussion in the design doc (the purpose of this PR) 🙈

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we cannot assume that MF2 authors will remember the details of how formatting options interact with exact variant keys during selection, if and when that behaviour differs from what happens during formatting

I disagree, from the perspective that they already kind of do. The behavior difference isn't that great and is mostly confined to locale-based formatting details (grouping, shaping, separator characters, etc.). Most of these folks are technical enough to know that -123.45 refers to a negative number with a specific value. Is it really that unintuitive what that key means?

I don't think there's any issue about understanding what a key like -123.45 means, but only about how it matches :number values. I think that can be made most intuitive by considering it a purely numeric value, and comparing it against the numeric value that's being formatted, and not taking into account some small subset of its formatting options.

How could an input variable have a formatting option pre-attached (roundingMode is an Intl.NumberFormat option, no?)? The formatting options are part of the formatter or some expression visible in the message.

That's enabled by using "an implementation-defined type" as per

The _operand_ of a number function is either an implementation-defined type or
a literal whose contents match the `number-literal` production in the [ABNF](/spec/message.abnf).
and effectively required to support attaching non-localizable options to number operands. In the JS implementation, you could do it like this:

const mf = new Intl.MessageFormat('en', '{$num :number maximumFractionDigits=2}')
const num = { valueOf: () => 123.456, options: { roundingMode: 'floor' } }
mf.format({ num }) // '123.45'

This makes sense for options like roundingMode, which may be required to always use a specific value due to external constraints.

Note that the keys match use cases that the message author is trying to fulfill. Exact match keys need to "exactly" match the perceived value, which, as I think this thread shows, is influenced by formatting options.

No, they don't. They need to predictably match, so that a developer doesn't need to refer to the MF2 docs every time when they try to use exact matching. When dealing with a corner-case message like one needing to match 120 exactly while using significant-digit rounding, it's okay for the developer to need to round that value outside MF2 to get the behaviour that they want, rather than doing all the work within the message -- the formatting of a message will always happen within some general-purpose programming language, and problems can be solved there. Remember, supporting "all grammatical features of all languages" is a non-goal for us.

Comment on lines +539 to +543
### Standardize the Serialization Forms

Using the design above, remove the integer-only and no-sig-digits restrictions from LDML45
and specify numeric matching by specifying the form of matching `key` values.
Comparison is as-if by string comparison of the serialized forms, just as in LDML45.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this alternative leaves "specifying the form of matching key values" as undefined, I can't tell what selecting this alternative would mean. This should be either dropped, or defined more precisely.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that I didn't flesh this out fully, depending instead on the example that we were discussing just above. I will add the details here.

Note well: I'm not married to this as the design, but I'm trying to get at the technical requirements, especially the expectations of message authors (including translators)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs resolution.

@eemeli points out a number of gaps or infelicities in the current specification
and there was extensive discussion of how to address these gaps.

The `key` for exact numeric match in a variant has to be a string.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how this results from the requirements.

In some cases the key has to be a string, in other cases it is enough to be a number.
So the whole section below is only one option: IF we consider the keys to be stings, then ...

The idea that the key can be a number sometimes is not considered.

But it would be natural to map "...foo {}..." and "...|foo| {}..." in syntax to strings, and "...123 {}..." and "...|123| {}..." in syntax to numbers.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key has to be a string because the message is a string. The next line addresses this: if the key is a string, then the format of the string has to be clear so that it can be related to a number.

```
.local $num = {123.456 :number maximumSignificantDigits=2 maximumFractionDigits=2 minimumFractionDigits=2}
.match {$num}
120.00 {{This matches}}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exact matching is rarely used, and not for situations like this.

It is usually handy for cases like "1 {this is the last day}" or "1 {you got the gold medal}"
In these cases the value itself is not even rendered in the message.

Cases like "1 dollar" / "1.00 dollars" are handled properly by the plural rules and they don't need exact matches (one {{$amount} dollar" other "{$amount} dollars})

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be thinking too narrowly about usage. Yes, a lot of custom messages are of the "You have no..." (aka =0) or "This is your last..." (aka =1) variety. But there are also many messages that do insert the value (rather than forcing the translator to type it, e.g. "You received a reward when you accumulated {$points} points.") or cases where the message is tied to other (still-specific) values. "Many" is a relative term, of course. There are orders of magnitude fewer of these types of messages than plain 0/1 messages and orders of magnitude fewer of those than "insert number here" plural category ones. That doesn't make them unimportant.

In fact, to me that makes them more important, since those messages should work consistently with user experience of simpler values. People make mistakes when they can't just do what they would do "normally".

@mihnita mihnita added the blocker-candidate The submitter thinks this might be a block for the Technology Preview label Aug 28, 2024
Also responds to the long discussion with @eemeli about significant digits by removing from the example.
This changes the status to "Re-Opened" and adds a link to the PR. Expect to merge this imminently, although discussion on number selection remains.
@aphillips
Copy link
Member Author

@eemeli Can you check this again? Per 2024-09-16 call we agreed to merge this with status 're-opened'

Copy link
Collaborator

@eemeli eemeli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this again, I note that there's a remaining incomplete action here, on fleshing out the added alternative.

exploration/number-selection.md Outdated Show resolved Hide resolved
Comment on lines +518 to +519
0.0 {{You have no apples}}
1.0 {{You have exactly one apple}}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
0.0 {{You have no apples}}
1.0 {{You have exactly one apple}}
0 {{You have no apples}}
1 {{You have exactly one apple}}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This depends on whether the fraction digits apply or not. It doesn't matter, because the context is proposing a separate :plural selector.

Comment on lines +539 to +543
### Standardize the Serialization Forms

Using the design above, remove the integer-only and no-sig-digits restrictions from LDML45
and specify numeric matching by specifying the form of matching `key` values.
Comparison is as-if by string comparison of the serialized forms, just as in LDML45.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs resolution.

@macchiati
Copy link
Member

I think the cleanest approach is to do what spreadsheets do, and allow for constants of the form 10%. Then you could have:

|0%| {{You have no apples}}
|50%| {{You have exactly half an apple}}
|100%| {{You have a whole apple}}

Co-authored-by: Eemeli Aro <eemeli@mozilla.com>
@aphillips aphillips removed blocker-candidate The submitter thinks this might be a block for the Technology Preview LDML46 LDML46 Release (Tech Preview - October 2024) labels Sep 17, 2024
@aphillips
Copy link
Member Author

This is a design change and it isn't going in v46. Removed blocker-candidate to avoid any confusion.

@mihnita
Copy link
Collaborator

mihnita commented Sep 18, 2024

Real case example.
People with access to the internal bug system can confirm that it's 100% real (yaqs/3558614463273762816)

Reported on MessageFormat (MF1):

Using Polish with this message
(I only added the [...] part to show exactly what is selected):

{reviewsCount, plural,
  one   {{reviewsCount, number, ::compact-short} opinia [one]}
  few   {{reviewsCount, number, ::compact-short} opinie [few]}
  many  {{reviewsCount, number, ::compact-short} opinii [many]}
  other {{reviewsCount, number, ::compact-short} opinii [other]}
}

And the bug was that the results were inconsistent.
Sometimes you would get "9,6 tys. opinii [many]" and sometimes "9,6 tys. opinie [few]".

The reason was that the decision for the plural is done on the full number (reviewsCount, plural), but the rendering was done on something transformed (in this case to thousands).

So plural of 9618 is many, but plural of 9623 is few.
But both of them render as "9,6 tys." because the result is in thousands.

Two points here:


We can't just compare exacts as "format to string and compare with the given string".
Because the formatted as a string would include the word for thousand.

And in some cases one would expect the exact comparison on the non-formatted & non-transformed value.

I would argue that one expects this to match for reviewsCount for 2024:

{reviewsCount, plural,
  2024 {WOW, you have the same number of opinions as the current year!!!}
  *    {{reviewsCount, number, ::compact-short} opinii}
}

Not the formatted value, which would be "2 thousands" or "2 K"
Or even the transformed value, which would be 2


Good news is that MF2 works as one would expect, even with exact matches.
And works even if you try it with ICU 75.1.

You can easily try this piece of code:

@Test
public void testPolishPluralMf2() {
    Locale locale = Locale.forLanguageTag("pl");

    // MF1, MessageFormat
    String pattern1 = "{reviewsCount, plural,"
            + "  =2024 {THIS YEAR!}"
            + "  one   {{reviewsCount, number, ::compact-short} opinia [one]}"
            + "  few   {{reviewsCount, number, ::compact-short} opinie [few]}"
            + "  many  {{reviewsCount, number, ::compact-short} opinii [many]}"
            + "  other {{reviewsCount, number, ::compact-short} opinii [other]}"
            + "}";
    MessageFormat mf1 = new MessageFormat(pattern1, locale);

    // MF2, MessageFormatter
    String pattern2 = ""
            + ".input {$reviewsCount :number icu:skeleton=compact-long}"
            + ".match {$reviewsCount :number}"
            + "  2024  {{THIS YEAR!}}"
            + "  one   {{{$reviewsCount} opinia [one]}}"
            + "  few   {{{$reviewsCount} opinie [few]}}"
            + "  many  {{{$reviewsCount} opinii [many]}}"
            + "  *     {{{$reviewsCount} opinii [other]}}"
            + "";
    MessageFormatter mf2 = MessageFormatter.builder()
            .setPattern(pattern2)
            .setLocale(locale)
            .build();

    // Testing
    int [] toTest = { 1, 18, 23, 9618, 9623, 1018, 1023, 18018, 23023, 2024 };
    String format = "%-8s %-28s %s%n";
    System.out.printf(format, "Value", "MessageFormat (MF1)", "MessageFormatter (MF2)");
    System.out.printf(format, "~~~~~", "~~~~~~~~~~~~~~~~~~~", "~~~~~~~~~~~~~~~~~~~~~~");
    for (int value : toTest) {
        Map<String, Object> arguments = Map.of("reviewsCount", value);
        System.out.printf(format, value, mf1.format(arguments), mf2.formatToString(arguments));
    }
}

With these results:

Value    MessageFormat (MF1)          MessageFormatter (MF2)
~~~~~    ~~~~~~~~~~~~~~~~~~~          ~~~~~~~~~~~~~~~~~~~~~~
1        1 opinia [one]               1 opinia [one]
18       18 opinii [many]             18 opinii [many]
23       23 opinie [few]              23 opinie [few]
9618     9,6 tys. opinii [many]       9,6 tysiąca opinii [many]
9623     9,6 tys. opinie [few]        9,6 tysiąca opinii [many]
1018     1 tys. opinii [many]         1 tysiąc opinii [many]
1023     1 tys. opinie [few]          1 tysiąc opinii [many]
18018    18 tys. opinii [many]        18 tysięcy opinii [many]
23023    23 tys. opinie [few]         23 tysiące opinii [many]
2024     THIS YEAR!                   THIS YEAR!

Corrected the MF1 message to use =2024 instead of 2024, and updated the result.
Thanks Adison.

But the rest of the matches are still incorrect.

@aphillips
Copy link
Member Author

@mihnita

Note that your MF1 example would work better if you correct the syntax for an exact match:

-    String pattern1 = "{reviewsCount, plural,"
-            + "  2024  {THIS YEAR!}"
+    String pattern1 = "{reviewsCount, plural,"
+            + "  =2024  {THIS YEAR!}"

Then the "this year" matching stuff works fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design Design principles, decisions formatting
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants