-
-
Notifications
You must be signed in to change notification settings - Fork 34
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
base: main
Are you sure you want to change the base?
Conversation
exploration/number-selection.md
Outdated
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?}} | ||
* {{ ... }} | ||
``` |
There was a problem hiding this comment.
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:
- Declare exact value matching to be implementation-specific.
- 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}}
* {{ ... }}
There was a problem hiding this comment.
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 is123
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.
There was a problem hiding this comment.
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) 🙈
There was a problem hiding this comment.
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 anIntl.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
message-format-wg/spec/registry.md
Lines 340 to 341 in 132d9c3
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). |
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.
### 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
exploration/number-selection.md
Outdated
``` | ||
.local $num = {123.456 :number maximumSignificantDigits=2 maximumFractionDigits=2 minimumFractionDigits=2} | ||
.match {$num} | ||
120.00 {{This matches}} |
There was a problem hiding this comment.
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}
)
There was a problem hiding this comment.
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".
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.
@eemeli Can you check this again? Per 2024-09-16 call we agreed to merge this with status 're-opened' |
There was a problem hiding this 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.
0.0 {{You have no apples}} | ||
1.0 {{You have exactly one apple}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0.0 {{You have no apples}} | |
1.0 {{You have exactly one apple}} | |
0 {{You have no apples}} | |
1 {{You have exactly one apple}} |
There was a problem hiding this comment.
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.
### 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still needs resolution.
I think the cleanest approach is to do what spreadsheets do, and allow for constants of the form 10%. Then you could have:
|
Co-authored-by: Eemeli Aro <eemeli@mozilla.com>
This is a design change and it isn't going in v46. Removed blocker-candidate to avoid any confusion. |
Real case example. Reported on MessageFormat (MF1): Using Polish with this message
And the bug was that the results were inconsistent. The reason was that the decision for the plural is done on the full number ( So plural of Two points here: We can't just compare exacts as "format to string and compare with the given string". 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:
Not the formatted value, which would be "2 thousands" or "2 K" Good news is that MF2 works as one would expect, even with exact matches. 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:
Corrected the MF1 message to use But the rest of the matches are still incorrect. |
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. |
This is to build up and capture technical considerations for how to address the issues raised by @eemeli's PR #842.