Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Adding a :number offset option #926

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

Closed
wants to merge 4 commits into from

Conversation

mihnita
Copy link
Collaborator

@mihnita mihnita commented Nov 7, 2024

Fix for issue #701

@mihnita
Copy link
Collaborator Author

mihnita commented Nov 7, 2024

TODO: add tests.

But I wanted to unblock the splitting of the registry in separate files.
Plus, the wording is the the part that might be controversial.
The tests, not so much.

@mihnita mihnita added functions Issue pertains to the default function set LDML46.1 MF2.0 Draft Candidate labels Nov 7, 2024
@mihnita mihnita changed the title Adding a :number offset option (#701) Adding a :number offset option Nov 7, 2024
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.

If the option is available on :number, it should also be on :integer.

OTOH, if it only supports whole number values, it might make sense to only allow on :integer; I'm not aware of any use case for it having been presented for non-integer values.

I also submitted #927 as the text around resolved value should be clarified.

Comment on lines +208 to +209
The _resolved value_ of the _expression_ is determined by first subtracting the numeric value of `offset` from the _operand_,
and then resolving the _expression_ on the calculated value.
Copy link
Collaborator

Choose a reason for hiding this comment

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

If the offset is applied to the numerical value of the resolved value and the offset option is also retained in the resolved options, doesn't that mean that it may get double-applied?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the trouble with using :integer (or :number) for both formatting and selection.

So selection you need both the offset, and the original value.
Because you test the exact values against the original value, and the keywords against the value - offset.

But for formatting the value - offset is enough, and you can drop the offset from the resolved options.

Which means that if one does

.input {$count :integer offset=2}
.match $count
...

then the resolved value for $count should have the offset.

If we separated the :input / :number from :plural this would have an easy answer: :plural keeps the resolved value and resolved options "as is", and :integer resolves the value to the value - offset and remove the offset from the resolved values.

I am not pushing to revert that decision.
We voted and all.

But I am open to suggestions, because I am not sure what to do here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Idea: leave the resolved value as is, and leave the offset in the resolved options.
Then the selection can see both.
And the "formatted value" (which we don't have a concept of, although it would be useful) can do the diff.

Trouble is that without the concept of "formatted value" we can't fully describe how the formatting works.

We have a section named "Numeric Value Selection and Formatting", but we don't seem to describe the formatting part at all.

Copy link
Member

Choose a reason for hiding this comment

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

My view is that the :offset affects selection and formatting (in parallel), but it is up to us to define in the :number specification for offset how successive :number offset values are combined — which does not prevent any other function options from combining in a different way.

Take the following:

.local $a {10 :number offset=2}
.local $b {$a :number offset=1}
.match $b
// please forgive any syntax errors

It is up to us to determine whether :number offset behaves like:

.local $a {10 :number :offset=3} // accumulates
or

.local $a {10 :number :offset=1} // replaces

I think we should have a general rule that unless otherwise stated, combinations of options do replacement, but that the specification for any given function option can override that behavior.

spec/registry.md Outdated
Comment on lines 159 to 160
- `offset` (optional)
- ([digit size option](#digit-size-options), default: `0`)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a reason why a negative offset is not allowed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I can't imagine a use case for it.
ICU works. But also works with fractional values. And negative fractional values. And with fractional arguments (see my other comment)

Copy link
Member

Choose a reason for hiding this comment

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

The purpose is to list the first n members of a list explicitly, then switch on the remaining number. Negative doesn't have any usage.

Copy link

Choose a reason for hiding this comment

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

ICU compatibility sounds like a good argument to me.

Copy link
Member

@aphillips aphillips left a comment

Choose a reason for hiding this comment

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

Partial review

@@ -449,14 +457,19 @@ Number selection has three modes:
- `ordinal` selection matches the operand to explicit numeric keys exactly
followed by an ordinal rule category if there is no explicit match

When the selection mode is `plural` implementations can optionaly support
Copy link
Member

Choose a reason for hiding this comment

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

Why wouldn't ordinal work the same way?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why wouldn't ordinal work the same way?

I think we discussed it in the last meeting and nobody was able to come up with a use case for ordinals.

But ICU supports it.
So I am open to add it.

Copy link
Member

Choose a reason for hiding this comment

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

list = { Audrey, Bill, Carlos, David, Edna, Freda}

.local $first = {$list :get item=1}
.local $second = {$list :get item=2}
.local $user = {$list :get item=last}
.local $length = {$list :length}
.local $position = {$length :integer select=ordinal offset=2} 
.match $position
one {{After {$first} and {$second}, {$user} is {$position}st in line}}
two {{After {$first} and {$second}, {$user} is {$position}nd in line}}
... etc...

... but it's a pretty contrived use case...

@mihnita
Copy link
Collaborator Author

mihnita commented Nov 7, 2024

If the option is available on :number, it should also be on :integer.

OTOH, if it only supports whole number values, it might make sense to only allow on :integer; I'm not aware of any use case for it having been presented for non-integer values.

I also submitted #927 as the text around resolved value should be clarified.

Good catch, thank you.

I added the option to :integer

About removing it from :number, I am split.

I can't imagine a use case for non-int with offset.

BUT I have two reasons to keep it:

One is that ICU supports it.

You can pass a non-int argument and get "Alice, Bob, and 3.142 other guests".
You can also pass a non-int offset, and works.
And you can pass a negative offset (integer or not) and works.

I cannot imagine a use case for any of these...

The second reason I kept it was that I suspect that some devs would not be too careful about :number and :integer.
So functionally they would only pass integral values, but do the formatting and selection with :number.


But I am fine to say: no, we don't support any of this.
We only support 0 or positive integers for offset, and only only support offset on :integer.

Adding them later would be backward compatible.

@mihnita
Copy link
Collaborator Author

mihnita commented Nov 7, 2024

Summarizing the questions:

  • should we support fractional and / or negative offsets?
  • should we support offset on :number?
  • should we support offset in ordinal mode?
  • what to do with offset in the resolved options?

@eemeli
Copy link
Collaborator

eemeli commented Nov 9, 2024

Thinking about this some more, I think copying the MF1 plural offset option seems like the wrong thing to do: We are not restricted in the number of functions that are available, or the number of selectors. Considering the canonical example case for offset, slightly modified to use one rather than =2:

{num_guests, plural, offset:1
=0    {{host} does not give a party.}
=1    {{host} invites {guest} to their party.}
one   {{host} invites {guest} and # other person to their party.}
other {{host} invites {guest} and # other people to their party.}}

I think one reason why the docs example doesn't use this formulation is that it showcases the weirdness of having the =1 and one operate on different values.

For MF2, yes, we could copy the structure directly, but that would retain the weirdness, and the reliance on looking up what actually happens with offset: Is it added or subtracted? Does it affect the exact value selection? Does it affect the # formatting?

Wouldn't it be easier to read if we separated the behaviour into another function and used two variables? As in:

.input {$num_guests :integer}
.local $num_other_guests = {$num_guests :math subtract=1}
.match $num_guests $num_other_guests
0 *   {{{$host} does not give a party.}}
1 *   {{{$host} invites {$guest} to their party.}}
* one {{{$host} invites {$guest} and {$num_other_guests} other person to their party.}}
* *   {{{$host} invites {$guest} and {$num_other_guests} other people to their party.}}

Would that not be much clearer about what each value is, what is being selected, and what is being formatted?

The proposed :math wouldn't need to support any other option than subtract to start with, and other operations could be added if and as use cases for them are presented.

@aphillips
Copy link
Member

@eemeli I had the same thought. Assigning a local integer and using the bog standard :integer selector feels like a cleaner way to implement this.

FWIW, since the primary use case is alleged to be working with lists, that suggests that the :list function will need helpers or options for accessing list items, list size, etc. An offset option might make sense with .local remaining = {$guestList :list-size offset=2}

@eemeli
Copy link
Collaborator

eemeli commented Nov 9, 2024

FWIW, since the primary use case is alleged to be working with lists, that suggests that the :list function will need helpers or options for accessing list items, list size, etc. An offset option might make sense with .local remaining = {$guestList :list-size offset=2}

In that case as well my preference would be to not overload a :list-size function with an offset option, but to have something separate like :math subtract that would make it explicitly clear to a translator whether the "offset" was being subtracted or added to the size.

@macchiati
Copy link
Member

I think it is a good ideal to handle the problem with a separate function. Moreover, it very much helps with clarity if we divide functions into two kinds:

  • functions whose options only affect formatting and selection (:number, :string)
  • functions whose options may produce a different 'core' value, like :math subtract=3 or :case type=uppercase

@aphillips
Copy link
Member

Moreover, it very much helps with clarity if we divide functions into two kinds:

Introducing non-selector/non-formatting functions does produce some complexity, since we require annotation in certain instances and implementations would have to check the type of function.

BTW "different 'core' value" has a name: "resolved value".

There are many obvious kinds of functions that could live here (cf. text-transform in CSS). Some of these are kind of like formatters ({$var :text-transform type=uppercase} selects and formats like :string, but with an uppercase transform, right?) while others would be problematic (:math wants :number/:integer/etc to do the formatting and probably doesn't want to directly be a selector)

@macchiati
Copy link
Member

I used the term 'core value' specifically because it is not the same as the resolved value.

.local $a = {123 :number style=decimal}
and
.local $b = {123 :number style=percent}

These have different resolved values because they have different options. See https://github.com/unicode-org/message-format-wg/blob/main/spec/formatting.md#:~:text=While%20this%20specification%20does%20not%20require%20it

I agree with pulling offset= out of :number, because it makes the delineation between the different kinds of functions much cleaner. For a function that only selects or formats or both, composition can be very simple: override the options. That is, given:

.local $x = {literal :osfFunction1 option1=ov1 option2=ov2}
.local $y = {$x :osfFunction1 option1=other option3=ov3}
.local $z = {literal :osfFunction1 option1=other option2=ov2 option3=ov3}

The $y and $z would have exactly the same resolved value. That is then a very predictable and understandable model for users. It doesn't shut out having other kinds of functions, like :math, that have more complicated composition, eg in the following $y1 and $z1 would have the same resolved values, which is not just a simple "override the options" model.

.local $x1 = {3 :math subtract=1}
.local $y1 = {$x1  :math subtract=1}
.local $z1 = {$x1  :math subtract=2}

@mihnita
Copy link
Collaborator Author

mihnita commented Nov 11, 2024

Introducing non-selector/non-formatting functions does produce some complexity, since we require annotation in certain instances and implementations would have to check the type of function.

I think "quite on the contrary"
Knowing the type of function helps (some) implementations.
The same way a type system makes certain programming languages less error-prone.

This is similar to the distinction between formatter functions and selector functions.
Which (I've always maintained) are different animals.

@aphillips
Copy link
Member

Replacing with :math. Thanks for working on this, @mihnita !!

@aphillips aphillips closed this Nov 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
functions Issue pertains to the default function set LDML46.1 MF2.0 Draft Candidate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants