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

Describe number selection fully #621

Merged
merged 34 commits into from
Feb 15, 2024
Merged

Describe number selection fully #621

merged 34 commits into from
Feb 15, 2024

Conversation

aphillips
Copy link
Member

@aphillips aphillips commented Feb 3, 2024

Update the design document as part of my action item in order to describe number selection completely per last week's call. The core text is intended to go somewhat verbatim into the spec.

[NOT FINISHED]

Update the design document as part of my action item in order to describe number selection completely per last week's call.
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
Comment on lines 211 to 219
A numeric literal key value exactly matches the resolved value of an operand as follows:

> this is still in progress, but basically has to consider:

- signs match
- minimum fraction digits match
- scientific notation?
- other formatting gorp doesn't apply (e.g. currency or measure unit)
- no grouping
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 we have :string separately, :number should so numerical comparison:

Suggested change
A numeric literal key value exactly matches the resolved value of an operand as follows:
> this is still in progress, but basically has to consider:
- signs match
- minimum fraction digits match
- scientific notation?
- other formatting gorp doesn't apply (e.g. currency or measure unit)
- no grouping
To determine if a numeric literal key value
exactly matches the resolved value of an operand,
the parsed numerical value represented by the key
is compared to the numerical value of the _operand_.
Selector options such as `minimumFractionDigits` have no effect on this comparison.

With that, the keys 1.0 and 1 would both match a number 1, with equal weight.

Copy link
Member Author

Choose a reason for hiding this comment

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

Floating point vs. integer representation might introduce some corner cases. I'm not sure about "equal weight" either. Our specification requires a list at the moment, so we should provide a tie-break so that the order is deterministic.

We also have to be cautious. Suppose the selector is {$num :number maximumFractionDigits=1}, the value in the operand is 1.0001, and the key is 1.0. This should probably be a match, right? Even though the numerical values are not (quite) equal?

Copy link
Member

@macchiati macchiati Feb 5, 2024

Choose a reason for hiding this comment

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

A couple of points:

  • As @aphillips notes, many languages have different plural selection behavior depending on the presence or absence of trailing zero fraction digits, so those must be maintained. That is, |1|, |1.0|, |1.00|, etc. may all select differently. So it is vital to maintain those distinctions.
  • The interaction between max/min digits and significant digits is surprisingly complicated. Describing the precise behavior should not be attempted in this spec.

Copy link
Collaborator

Choose a reason for hiding this comment

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

No, that should not match as the numerical values are not equal.

While I agree that the behaviour around non-integer values may be a little surprising, as far as I know they are only theoretical, and should not influence the overall design unduly. Or are there any real-world examples of current messages that need to work around the limitations of MF1, Fluent, or any other syntax that supports selection on exact numerical values? I have searched a bit, but have not found any.

Copy link
Member Author

Choose a reason for hiding this comment

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

@eemeli I didn't take you text verbatim, but I took the idea into account. Check it out.

@macchiati Thanks. The matching behavior for literal keys I don't think is affected by this, but certainly the plural/ordinal rule computation is. I think we've accounted for that.

I agree that we don't want to get into signfiicant digits and such, although I am worried about developers not being able to figure out why setting the number of digits or modifying the rounding mode doesn't cause operand values to match certain key literals (as @eemeli suggests). That is, why doesn't {|1.00000001| :number maximumFractionDigits=1} match 1.0 (since it formats as "1.0" or the equivalent)?

Copy link
Member

Choose a reason for hiding this comment

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

I think the the most robust approach would be the following description (I'm not completely clear if this is just restating what your words were or not).

A literal matches a variable using {$variable :number ...}, iff after formatting the literal using the same settings as the $variable has, there is an exact string match.

It can clearly be optimized in production, but if we state it like this it establishes the principle. It also means that we can detect that 1 are equivalent: will either both match or neither match, which can be detected statically.

Copy link
Member Author

Choose a reason for hiding this comment

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

Let me give it a shot at wording. I think this is the better approach.

Copy link
Member

@macchiati macchiati Feb 6, 2024

Choose a reason for hiding this comment

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

Mihai, good point about the two different ways in which the match is done for offsets; I'd neglected to consider that in my reply.

I think the hacky way is just too hacky. However, as long as the addition syntax needs adding anyway, we can just do the following:

When an offset option is is specified on :number (or :plural...), literals are matched by with an :exact match to the operand (eg $number in the example below)*, while both plural categories and formatting of the operand are interpreted as if the operand had the offset value subtracted from it.

.match {$number :number offset=|1|}
   0     {{Invited no guests}}
   1     {{Invited only {guest1}}}
   one   {{Invited {guest1} and {$number} more guest}}
   other {{Invited {guest1} and {$number} more guests}}

Copy link
Member Author

@aphillips aphillips Feb 6, 2024

Choose a reason for hiding this comment

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

MF2 has multiple selection and that might work better than trying to shove everything into a single selector:

.local $offset = {$numGuests :number offset=1}
.match {$numGuests :number} {$offset}
0   *   {{No guests were invited}}
1   *   {{Only {$guest1} was invited}}
*   one {{{$guest1} and {$offset} more guest was invited}}
*   *   {{{$guest1} and {$offset} more guests were invited}}

(Note that other languages would write one one/one few/one */etc. messages to fit grammatical needs. The keys 0 * and 1 * act as guardrails against the weird behavior of $offset)

Copy link
Member

Choose a reason for hiding this comment

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

Good point; that would involve just one additional operand ('offset', or maybe 'minus'). Not terse, but works.

.local $offset = {$numGuests :number offset=1}
.match {$numGuests :number} {$offset}
0   *   {{No guests were invited}}
1   *   {{Only {$guest1} was invited}}
*   one {{{$guest1} and {$offset} more guest was invited}}
*   *   {{{$guest1} and {$offset} more guests were invited}}

@aphillips aphillips requested a review from eemeli February 4, 2024 15:56
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
Comment on lines 141 to 142
Implementations MUST treat literals that do not match `number-literal` as
non-numeric string literals.
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should be careful not to establish different rules for literal values and variables with string values. Doing so would mean that the value that's passed in to a function implementation would need to distinguish between the two, and therefore could not use e.g. a primitive string value, but would need some sort of wrapper.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed. Perhaps:

Suggested change
Implementations MUST treat literals that do not match `number-literal` as
non-numeric string literals.
Implementations MUST treat literals that do not match `number-literal` as
plain string literals.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Your fix doesn't seem to address my issue? The questionable word in this phrase is "literal", because in our common usage it's referring to a value that's defined directly in the syntax source. My concern is that the spec should allow for an implementation to consider the values of e.g. a literal operand 42 and a variable operand with the resolved value of the string 42 to be indistinguishable from each other.

Copy link
Member

@macchiati macchiati Feb 7, 2024

Choose a reason for hiding this comment

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

That gets into what the "resolved" value is. The *formatted" value of a variable operand might be ४२ (numerically equal to 42, but not equal in string value).

Note: I'm not saying we have to accept |४२| as equivalent to |42|. But I think we are fuzzy about what 'resolved' value means.

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 think my edits this morning (1c0bf19) address this. I removed the MUSTard in favor of text very similar (intentionally similar) to that in date/time.

@macchiati "resolved value" means mostly that we unwind the chain of assignments to have the actual value in hand. That is:

Input: `{ 'a' : 'wildebeest'}`
.local $b = {$a}
.local $c = {$b}
.local $d = {$c}
{{The resolved value of {$d} is the literal 'wildebeest'}}

This is not the same thing as the formatted value, although any annotations pick up along the way are available.

FWIW, |४२| is not a valid numeric literal because it does not match the numeric-literal production. It's just some string.

Comment on lines 143 to 146
Non-numeric values, including non-numeric string literals, result in
a _Selection Error_.

> _Ed note:_ Alternatively we can require that non-numeric values result in `*`
Copy link
Collaborator

Choose a reason for hiding this comment

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

According to our current formatting spec, non-numeric input would first cause a resolution error, which means that the expression uses fallback resolution. A selection error may also be emitted later when the resolved fallback value is used for selection, which it doesn't support.

Suggested change
Non-numeric values, including non-numeric string literals, result in
a _Selection Error_.
> _Ed note:_ Alternatively we can require that non-numeric values result in `*`
Non-numeric values, including non-numeric string literals,
result in a _Resolution Error_,
and use _Fallback Resolution_ to determine the resolved value of the _expression_.

Copy link
Member Author

@aphillips aphillips Feb 6, 2024

Choose a reason for hiding this comment

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

For a selector, there is no pattern yet. In fact, the text in the formatting part of the spec is biased to formatting functions. I agree, in general, that probably is a resolution error. However, if it were strictly in a selector (not .local/.input) we're probably prefer selection error??

The only recourse when there is a resolution error in a selector is to fallback to the logo--because we don't know what pattern to use. Our text currently says:

Otherwise: the U+FFFD REPLACEMENT CHARACTER �

This is not currently used by any expression, but may apply in future revisions.

Copy link
Collaborator

Choose a reason for hiding this comment

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

However, if it were strictly in a selector (not .local/.input) we're probably prefer selection error??

For cases where multiple errors ought to be emitted, we currently only say this:

When a message contains more than one error, or contains some error which leads to further errors, an implementation which does not emit all of the errors SHOULD prioritise Syntax Errors and Data Model Errors over others.

I don't think we have the time before LDML45 to specify error emission order further, and I think the spec gets unduly complex if we need to mess with it.

The only recourse when there is a resolution error in a selector is to fallback to the logo--because we don't know what pattern to use.

We use the * pattern. First in Resolve Selectors 2.iii. nomatch is added to res for the selector that failed to resolve, which means that the pref result of Resolve Preferences is a list with a single empty list as its contents ([[]]). This means that in Filter Variants only the * variant gets picked up as a filtered variant (checked in step 2.i.b.), which makes its sorting pretty easy.

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'll buy that.

I don't think this can be a Resolution Error, since the value resolves correctly. It is the number function that is not accepting the input. In the case of a selector, this means that it can produce a Selection Error (to indicate the type mismatch). In the case of a formatter, this means that it can produce a Formatting Error. If you read the list of Resolution Errors, you'll see that none of them fit this case. And I do think that it is internal to :number (and friends) whether to accept a given value.

The behavior of the selector is as you suggest, with the * case being the pattern selected. If the value is used in the pattern, that value will use its fallback resolution. Either way there will be an error available to be emitted (in an implementation-defined way).

Copy link
Collaborator

Choose a reason for hiding this comment

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

While the operand value does resolve correctly in this case, it's the resolution of the expression that fails. Consider this nonsensical but formally valid message, for instance:

.local $x = {horse :number}
.match {$x}
1 {{I'm a cow}}
* {{Herding {$x} along}}

Here, we need to emit an error when resolving the {horse :number} expression, and then when using its value for selection. That same order of errors should be retained if the selector expression itself fails to resolve.

If you read the list of Resolution Errors, you'll see that none of them fit this case.

Good point. The specific kind of resolution error isn't currently defined:

If the call fails or does not return a valid value,
emit a _Resolution Error_ and use a _fallback value_ for the _expression_.

Copy link
Member Author

Choose a reason for hiding this comment

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

@eemeli Thanks for this.

I think your example is a good one, since we want the expression to fail in the .local rather than waiting for the .match or the formatting in the pattern.

Regarding the quoted lines, I'm not sure, having thought about it, what an "invalid value" is (we don't define a valid value). null might be an acceptable value for some kinds of calls? Ultimately, though I think we need a specific error type (so we can test for it, if nothing else). I think something like Invalid Expression, one subtype of which would be Type Mismatch.

I'll create an issue to track and a PR (if I have time) to propose it.

exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
Comment on lines 318 to 321
The resolved value of an `operand` exactly matches a numeric literal `key`
if, when the `operand` is serialized using the format for a JSON number
including the exact number of fraction digits specified by the selector
function or its options, the two strings are equal.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This places an unreasonable burden on all selections in order to support a theoretical corner case. Consider, for instance, that we also support the options minimumIntegerDigits, minimumSignificantDigits, and maximumSignificantDigits. They either need to be considered here as well, or ignored.

I'm starting to think we don't need to specify this here, and should leave it up to implementations. Comparisons against floating point values are fraught in general, and should be avoided.

Suggested change
The resolved value of an `operand` exactly matches a numeric literal `key`
if, when the `operand` is serialized using the format for a JSON number
including the exact number of fraction digits specified by the selector
function or its options, the two strings are equal.
The resolved value of an `operand` exactly matches a numeric literal `key`
if, when the `operand` is serialized using the format for a JSON number,
the two strings are equal.
All formatting options are ignored when applying this serialization.
Using keys representing fractional numbers is not recommended,
and may produce different results with different implementations.

If this suggestion is accepted, the examples below will also need to be updated.

Copy link
Member Author

Choose a reason for hiding this comment

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

Integer and significant digits (and, fwiw, grouping) formatting specifications don't enter into it because they don't affect plural selection the way that fraction digits do. They surely affect the appearance of the final product, but not "equality".

This means that overlong keys probably don't work:

.match {$num :number minimumIntegerDigits=3}
000 {{I can never be selected}}
010 {{I can't either}}
100 {{But I work}}

Comparisons against floating point values are fraught in general, and should be avoided.

Yes. I'll note that people using this feature are on thin ice (this is actually the one place where ChoiceFormat did something useful in the modern era). Nearly all exact number selections that I'm familiar with use integers.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, I'll grant you that minimumIntegerDigits can be discounted, but:

new Intl.NumberFormat('en', { maximumSignificantDigits: 2 }).format(1.03) // "1"
new Intl.NumberFormat('en', { maximumSignificantDigits: 3 }).format(1.03) // "1.03"

new Intl.NumberFormat('en', { minimumSignificantDigits: 2 }).format(1.3) // "1.3" 
new Intl.NumberFormat('en', { minimumSignificantDigits: 3 }).format(1.3) // "1.30"

Copy link
Member

Choose a reason for hiding this comment

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

This places an unreasonable burden on all selections in order to support a theoretical corner case

I disagree. The suggested language for this will break for some cases, and not just theoretical ones.

The language "Apply the rules defined by CLDR to the resolved value of the operand and the function options,
and return the resulting keyword." works, and is not an excessive burden. Worst case is that as well as formatting the variable, you also format a few literals. However, it is very easy to optimize the implementations to avoid the extra formatting.

Copy link
Member Author

Choose a reason for hiding this comment

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

@eemeli Fair enough. I added significant digits to the match, including examples.

I do notice that both FF and Chrome's implementation of Intl.NumberFormat prioritizes significant digits ahead of fraction ones:

const number = 1.23456;

console.log(
  new Intl.NumberFormat('de-DE', { maximumSignificantDigits:3, maximumFractionDigits:0 }).format(
    number,
  ),
);

Produces: 1,23

(ICU4J NumberFormatter does not replicate this, so this might be a bug)

        NumberFormatter.withLocale(Locale.US)
           .precision(Precision.minSignificantDigits(3))
           .precision(Precision.maxFraction(0))
           .format(1.2345)
           .toString()  // produces `1`
       );

In any case, I didn't deal with the interplay between these two in the spec and * sigh * I probably need to.

Copy link
Member

Choose a reason for hiding this comment

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

@sffc is the expert on this. The best interplay between them where there are seeming conflicts is tricky to get right, and as I recall, not just a matter of having one have precedence over the other.

Copy link
Member

Choose a reason for hiding this comment

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

My previous research showed that min/max significant digits are much less popular than min/max fraction digits outside of compact notation. If you don't need compact notation, I would suggest considering dropping significant digits.

There is a way to map between ECMA-402 digit options and NumberFormatter digit options. I maintain that the NumberFormatter options are simpler and less error-prone, but the ECMA-402 options were set long ago so we did the best we could within that space.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks @sffc. Significant digits are definitely going to be rarer than fraction digits--I can't recall using them in any kind of "normal" application, aside from compact (and then only sporadically).

I have no problem with ECMA-402's options. I just note that it produced some surprise for me to get a fractional part after I explicitly asked not to (e.g. significant > fraction). I expected that significant digits would step on the value of the formattable number, while fraction digits stepped on the number of decimal places shown.

We only have those options that overlap between 402 and ICU4J (so, for example, no rounding control) and since significant digits is in both, that's why it's there. I would shed zero tears to drop it in preview, although it's just going to come back again later (or in the form of :icu:maximumSignificantDigits/:js:maximumSignficantDigits 😉)

In any case, we have differences in implementations, so I should tread carefully in MFv2 and make the interplay implementation defined (assuming you do not agree that ECMA-402's behavior is a bug).

Copy link
Member

Choose a reason for hiding this comment

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

We only have those options that overlap between 402 and ICU4J (so, for example, no rounding control)

ICU4J is a superset of ECMA-402. Note that ECMA-402 contains rounding increment and rounding mode as of NFv3 which was stabilized about a year ago.

In any case, we have differences in implementations, so I should tread carefully in MFv2 and make the interplay implementation defined

I don't agree with the characterization that there is a "difference in implementations". Rather, there is a difference in APIs. It should not be assumed that just because there is an option with the same / similar name in ECMA-402 and ICU4J that the two options have the same / similar behavior. When the APIs were designed (I was involved with both), the respective committees preferred to learn from the mistakes of the previous generation than try to match it bug-for-bug.

Copy link
Member

Choose a reason for hiding this comment

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

If it helps, in my opinion, there are two fully-defined API specifications which MF could choose to follow:

  1. Number Skeletons (ICU)
  2. Intl.NumberFormat (ECMA-402)

Or, MF could define its own surface.

Note: My original idea from 5 years ago was that MF would use number skeletons and then become the specification that defines number skeletons since they are currently specified only in the ICU documentation.

Co-authored-by: Eemeli Aro <eemeli@mozilla.com>
- `ordinal`
- `exact`
- `compactDisplay`
- `short` (default)
Copy link
Member

Choose a reason for hiding this comment

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

question, isn't the default for compactDisplay=none? That is, for the other cases, the default means "what happens if the option is not present"

Copy link
Member Author

@aphillips aphillips Feb 14, 2024

Choose a reason for hiding this comment

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

MDN says:

Whether to use short or long form when using compact notation. This is the value provided in the options.compactDisplay argument of the constructor, or the default value: "short". The value is only present if notation is set to "compact", and otherwise is undefined.

So probably this option should be moved under (depend on) notation

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. See above.

- `false`
- `minimumIntegerDigits`
- (non-negative integer, default: `1`)
- `minimumFractionDigits`
Copy link
Member

Choose a reason for hiding this comment

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

minimum default is zero?

Copy link
Member Author

Choose a reason for hiding this comment

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

These items (min*/max*) don't have defaults because they are overrides. The default is implementation defined.

- (non-negative integer, default: `1`)
- `minimumFractionDigits`
- (non-negative integer)
- `maximumFractionDigits`
Copy link
Member

Choose a reason for hiding this comment

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

is there a maximum? Or if unstated would I see 0.3333333333333333333333333333333333 for the value of 1/3?

Copy link
Member

Choose a reason for hiding this comment

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

etc

Copy link
Member Author

Choose a reason for hiding this comment

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

see above

- `false`
- `minimumIntegerDigits`
- (non-negative integer, default: `1`)
- `minimumFractionDigits`
Copy link
Member

Choose a reason for hiding this comment

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

It seems like the min/maxFractions default to 0 for integer, those being the only difference from :number

Copy link
Member Author

Choose a reason for hiding this comment

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

They default to zero and cannot be set for :integer

Comment on lines 218 to 219
- `minimumSignificantDigits`
- (non-negative integer)
Copy link
Member

Choose a reason for hiding this comment

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

Please remove minimumSignificantDigits from :integer; it serves no purpose other than to create confusion when adding trailing zeros which it seems we want to avoid in :integer mode.

Copy link
Member

Choose a reason for hiding this comment

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

You went too far; I am actually okay with maximumSignificantDigits on :integer, just not minimum* because the only purpose that serves is to create trailing zeros which are not needed in :integer mode.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

Comment on lines 367 to 372
The resolved value of an `operand` exactly matches a numeric literal `key`
if, when the `operand` is serialized using the format for a JSON number
the two strings are equal.
Implementations SHOULD take into account including the exact number of fraction digits
and the exact number of significant digits specified by the selector
function or its options.
Copy link
Member

Choose a reason for hiding this comment

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

I'm not really comfortable with this type of hand-wavy language. We have established that there are multiple ways to process min/max fraction/significant digits, which differ between ICU and ECMA-402. Switching implementations should not cause my rule selection to change. I suggest that we use ECMA-402 rules for merging fraction/significant options, which are defined here:

https://tc39.es/ecma402/#sec-setnfdigitoptions

Alternatively, if we don't like that algorithm, remove significantDigits from the tech preview and this problem largely goes away.

Alternatively, why do we need exact literal match? This seems like ingredients for failure especially with floating point. It would be simpler and less error-prone if we either did not have this or if we supported only integer matching.

Copy link
Member Author

Choose a reason for hiding this comment

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

Significant digits alone doesn't resolve the problem.

We need to define exact literal match in order for cases like .match {$num} 0 {{You have no zebras}} to work, where 0 is the key. To your point, that is integer matching and I included a note about it:

Important

The exact behavior of exact literal match is only defined for non-zero-filled integer values. Annotations that use fraction digits or significant digits might work in specific implementation-defined ways. Users should avoid depending on these types of keys in message selection.

What the SHOULD text you quote says, basically, is that implementations ought to provide more match behavior for the (exceedingly rare, I would think) cases in which a fractional value is needed for a match. I could relax this to MAY with no loss of functionality.

Copy link
Member

Choose a reason for hiding this comment

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

I would be happy if you made such non-integer-match behavior optional and removed the examples that imply it is supposed to work a certain way.

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 problem here is message portability. Probably we should remove the SHOULD and just tell users that only integer exact match works in Tech Preview "but please write to us if you find an application for fraction digits"

I'll remove the examples once we've decided on what we're doing here. They're useful illustrations for the discussion.

Copy link
Member Author

Choose a reason for hiding this comment

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

(update) I implemented my comment in a8746ed

Co-authored-by: Shane F. Carr <sffc@google.com>
numeric selectors perform as described below.

- Let `return_value` be a new empty list of strings.
- Let `operand` be the resolved value of the _operand_.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
- Let `operand` be the resolved value of the _operand_.
- Let `operand` be the decimal value of _operand_ after formatting options from `:number` have been applied conforming to [the `sampleValue` grammar of UTS 35](https://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax).

The intermediate formatted value may have notation information associated with it, such as "1.0c3" for "1.0K" or potentially "2.3e4" for "2.3*10^4". We should be clear that this is the amount of information we want from the intermediate value, and it also allows us to select on compact values, which may be important in French, for example.

Copy link
Member Author

Choose a reason for hiding this comment

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

"resolved value" has special meaning in our specification (even though it is not highlighted here). It's important to say that operand is the "resolved value". We can add requirements after that, such as what you suggest.

Note that operand is untyped by us. It is either an object or it is a literal.

We say what to do with the literal (which ones are supported and how to parse them)

We mustn't define what the object is. That's up to the implementation. The implementer is responsible for resolving the decimal value, if that's what they need.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, are you referencing this language?

The form of the resolved value is implementation defined and the
value might not be evaluated or formatted yet.
However, it needs to be "formattable", i.e. it contains everything required
by the eventual formatting.

So the "resolved value" may or may not be the intermediate value?

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.

Overall fine, some suggested improvements inline.

exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
exploration/number-selection.md Outdated Show resolved Hide resolved
aphillips and others added 2 commits February 15, 2024 06:48
Co-authored-by: Eemeli Aro <eemeli@mozilla.com>
Co-authored-by: Eemeli Aro <eemeli@mozilla.com>
Copy link
Member

@sffc sffc left a comment

Choose a reason for hiding this comment

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

This PR is acceptable to me now, though I would still like to clarify the content of the resolved value for the purposes of literal selection.

Co-authored-by: Eemeli Aro <eemeli@mozilla.com>
@aphillips aphillips merged commit 3660793 into main Feb 15, 2024
1 check passed
@aphillips aphillips deleted the aphillips-number-selection branch February 15, 2024 17:37
eemeli added a commit to messageformat/messageformat that referenced this pull request Feb 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants