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

Add a :math function, for addition and subtraction #932

Merged
merged 4 commits into from
Nov 15, 2024
Merged

Conversation

eemeli
Copy link
Collaborator

@eemeli eemeli commented Nov 12, 2024

Fixes #701.
An alternative to #926.

Adds :math as a new numeric function that can add or subtract from the operand.

Note that the option values are more strict than for other functions; option values beyond "digit size option" are considered errors.

@eemeli eemeli added registry Issue pertains to the function registry LDML46.1 MF2.0 Draft Candidate labels Nov 12, 2024
@@ -381,6 +381,69 @@ together with the resolved options' values.

The _function_ `:integer` performs selection as described in [Number Selection](#number-selection) below.

### The `:math` function

The function `:math` is a selector and formatter for matching or formatting
Copy link
Member

Choose a reason for hiding this comment

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

It's not a formatting function, is it? A formatting function would require all of :number or :integer's options, e.g. to control zero digits, separators, and the like.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As proposed, you would be able to format a :math annotated value, so doesn't that make it a formatting function? If you need the options, you can set them in a separate declaration.

spec/registry.md Outdated Show resolved Hide resolved
spec/registry.md Show resolved Hide resolved

#### Selection

The _function_ `:math` performs selection as described in [Number Selection](#number-selection) below.
Copy link
Member

Choose a reason for hiding this comment

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

Should this make it clear that the selection is done after the math is performed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We could, but it's not technically necessary, as Number Selection includes this:

When implementing MatchSelectorKeys(resolvedSelector, keys) where resolvedSelector is the resolved value of a selector and keys is a list of strings, numeric selectors perform as described below.

which ends up referring back to the preceding Resolved Value section, which clarifies when the modifications are done on the resolved value.

Copy link
Member

Choose a reason for hiding this comment

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

The following still bothers me:

If the operand of the expression is an implementation-defined numeric type,
such as the resolved value of an expression with a :number or :integer annotation,
it can include option values.

I think we should replace that by:

If the operand of the expression is an implementation-defined numeric type,
such as the resolved value of an expression with a :number or :integer annotation,
it includes their options.

I really don't think we want the following two to differ in selection behavior, formatting, etc.

.local $y = {|3| :number maximumSignificantDigits=2}
.local $x = {$y :math subtract=1}
.local $x = {|2| :number maximumSignificantDigits=2}

So I'd recommend that the resolved value of :math inherit both the 'datatype' and the options from its operand.

spec/registry.md Show resolved Hide resolved
Co-authored-by: Addison Phillips <addison@unicode.org>
Copy link
Member

@macchiati macchiati left a comment

Choose a reason for hiding this comment

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

Whoa, I must of missed this earlier. It is seriously wrong.

#### Selection

The _function_ `:math` performs selection as described in [Number Selection](#number-selection) below.

Copy link
Member

Choose a reason for hiding this comment

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

I see some significant problems in these formulations.

The resolved value of an expression with a :number function
contains an implementation-defined numerical value
of the operand of the annotated expression,
together with the resolved options' values.

If that is all the resolved value is, then there is no difference in resolved value between the following two.

.local $x1 = {$x :integer}
.local $x2 = {$x :number}

But of course there is a big difference: the resolved value of $x1 contains a :integer annotation and the resolved value of $x2 contains a :number annotation, as in:

such as the _resolved value_ of an _expression_ with a `:number` or `:integer` _annotation_,

And it has to be that way, otherwise no .local setting later on could tell the difference between them.

So I think L142 of formatting.md is insufficient:

interface MessageValue {
  formatToString(): string
  formatToX(): X // where X is an implementation-defined type
  getValue(): unknown
  resolvedOptions(): { [key: string]: MessageValue }
  selectKeys(keys: string[]): string[]
}

It also needs do have something like: getFunctionAnnotation();

In shorthand, the resolved value needs to logically contain a triple {datatype, functionAnnotation, optionMap}. Otherwise, a subsequent .local statement using a resolved value wouldn't have enough information to know how to compose.

So let's look at the following:

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).
All other values produce a _Bad Operand_ error.

But the operand can also be a resolved value, which is neither a literal nor just an implementation datatype; it has additional information.

And in multiple cases in this file, we have wording like:

Resolved Value
The resolved value of an expression with a :number function contains an implementation-defined numerical value of the operand of the annotated expression, together with the resolved options' values.

Now, if somewhere we say that the resolved value of any expression with a function contains that function as an annotation, then we could get away with this shorthand (although we also need to fix the wording for options).

Otherwise, we need to say:

The resolved value of an expression with a :number function contains an implementation-defined numerical value of the operand of the annotated expression, together with the :number function annotation, and the options with their respective resolved option~'s~ values.

Copy link
Member

Choose a reason for hiding this comment

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

I don't get it.

The composition model is that functions can expose specific options, but the identity of the function itself is not passed. There is no difference between the resolved values of {$x :number} and {$x :integer} with regard to the receiving function: it's just a numeric type. A subsequent local declaration doesn't need to know which annotation was previously applied: the preceding function handler modified the resolved value and/or passed the relevant options to communicate what happened.

It has to be that way, because otherwise each function would need to understand what all of the other functions (including namespaced ones) "mean".

If {$x :math subtract=2} doesn't return $x - 2 as its resolved value, :number isn't going to know what to do.

There are places where I have actively resisted "doing harm" to the resolved value based on the annotation. That's an argument we can have. Mostly I'm concerned that we don't strip information from the resolved value because of some formatting instructions:

.input {$now :date style=medium}
.local $time = {$now :time style=short}
{{"Today is {$now} and it is {$time}" only works if :date doesn't remove the time.
    I assume that is okay because immutability says that if $now is a Date object or ZonedDateTime
    there is no need to convert it to just a ZonedDate and make the time midnight.}}

We could change :integer to make the resolved value the floor value of the operand or to make it the implementation-defined integer-type casting of the operand. That doesn't break immutability because the resolved value is only exposed in a declaration. I think that's the better course than what you're proposing.

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 I conflated too many thoughts in my message. So put on hold the notion of the function annotation (not needed for release) and I focus on just 2 issues.

  1. The operand for a function may also be a resolved value (which includes an optionMap). That is not the same as an implementation defined type (eg Date); a resolved value can have an implementation-defined type. Thus

OLD

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.
All other values produce a Bad Operand error.

needs to be changed to:

NEW

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,
or a resolved value.
All other values produce a Bad Operand error.

(with similar changes for other definitions of operand of an X function.)

  1. The expression "together with the resolved options' values." is not accurate. For {|3| :number roundingIncrement=5 trailingZeroDisplay=auto}, that would imply a logical result of

{operand=3, resolvedOptionValues=[5, auto]}
rather than the logical result of

{operand=3, resolvedOptionMap={roundingIncrement=5, trailingZeroDisplay=auto}}
So we need to have:

OLD

... value of the operand of the annotated expression, together with the resolved options' values.

changed to something like:

NEW

... value of the operand of the annotated expression, together with a map from options to their resolved values

Copy link
Member

Choose a reason for hiding this comment

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

The problem with the first item is that the resolved value might not be of the correct implementation-defined type. The current text accounts for this by not mentioning. We could say instead:

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
(either of which can be the resolved value of a previous declaration).
All other values produce a Bad Operand error.

The second suggestion we already discussed. Saying "map" was thought too prescriptive (ditto "array", "list", etc.). I can see how one might read "options' values" to mean only the values and not the k-v pairing. We might say:

... value of the operand of the annotated expression, together with options and their resolved values.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One difference between {$x :number} and {$x :integer} is that the resolved value of the former holds a numeric value, while the resolved value of the latter holds an integer value. Given that :integer also strips out a pre-defined set of options if they're included in the operand, and we don't mandate that the actual type of the number values match, I don't see a reason why anything in MF2 requires the resolved values to be more identifiable from each other?

The operand for a function may also be a resolved value (which includes an optionMap). That is not the same as an implementation defined type (eg Date); a resolved value can have an implementation-defined type.

The intent with the current language is that a resolved value can be considered as "an implementation defined type". We do not require an implementation to only define a single type that it supports, so both a Date and the resolved value of a :datetime annotation can simultaneously fit into that definition.

Copy link
Member

Choose a reason for hiding this comment

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

From Addison

The problem with the first item is that the resolved value might not be of the correct implementation-defined type. The current text accounts for this by not mentioning. We could say instead:

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
(either of which can be the resolved value of a previous declaration).
All other values produce a Bad Operand error.

Wouldn't quite work. Again, we have defined the resolved value of {|3| :number style=percent} to be NOT the same as |3| — since the resolved value includes options. I think what we could say:

(either of which can be from the resolved value of a previous declaration).

I think that is at least headed in the right direction, though not as correct as

or a resolved value with those as an operand.

The second suggestion we already discussed. Saying "map" was thought too prescriptive (ditto "array", "list", etc.). I can see how one might read "options' values" to mean only the values and not the k-v pairing. We might say:

... value of the operand of the annotated expression, together with options and their resolved values.

That sounds perfect.

Copy link
Member

Choose a reason for hiding this comment

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

@macchiati

Wouldn't quite work. Again, we have defined the resolved value of {|3| :number style=percent} to be NOT the same as |3| — since the resolved value includes options. I think what we could say:

(either of which can be from the resolved value of a previous declaration).

I think this formulation is actually counterproductive. The operand is not extracted from the resolved value. The operand includes the accumulated options (which might affect the processing of the function or which might be filtered or ignored).

I agree that the operands in the two expressions in your comments have different resolved values, but one of the differences is that {|3| :number style=percent}'s resolved value is likely to be an implementation-defined numeric type (such as Java BigInteger or BigDecimal) and :math wouldn't have to parse the literal |3| again.

In any case, my formulation does not say "from" on purpose.

The later "perfect" quote relates to the resolved value of the :math function, which is the output of the function. My clarification moved the word 'resolved' to try to make clear that the options and their values were both resolved. But this is a different context from the operand discussion just above.

  • Operand: a naked argument or a resolved value (which is...)
  • Resolved value: a resolved (and possibly modified) operand plus resolved options and their resolved values

Oddly, this discussion appears to be about a stock formulation in all of our functions. Can we merge :math and then make any changes globally? Can I get your approval on this PR? I filed #937 to track this conversation so we make any agreed changes.

Copy link
Member

Choose a reason for hiding this comment

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

Sure.

@mihnita
Copy link
Collaborator

mihnita commented Nov 13, 2024

I really don't see how this is cleaner than the offset.

It looks very close to a :number with offset, except that the exact selection, the keyword selection, and the formatting are all done both on the same value.

Which seem cleaner as implementation, and logic, but less intuitive for a user:

.local $countDelta = {$guestCount :math sub=2}
.match $countDelta
   -2 {{No guest}}
   -1 {{{$guest1}}}"
    0 {{{$guest1} and {$guest2}}}"
  one {{{$guest1}, {$guest2}, and {$countDelta} other guest.}}"
    * {{{$guest1}, {$guest2}, and {$countDelta} other guests.}}"

As a user is not intuitive at all why would I select on -2 to get "No guest", or why a :math function does something that looks like plural selection.

spec/registry.md Show resolved Hide resolved
#### Selection

The _function_ `:math` performs selection as described in [Number Selection](#number-selection) below.

Copy link
Member

Choose a reason for hiding this comment

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

Sure.

Co-authored-by: Addison Phillips <addison@unicode.org>
@aphillips
Copy link
Member

I'm going to merge this. @mihnita (or anyone else) If you have concerns about the resulting text, please raise PRs or issues.

@aphillips aphillips merged commit b6ba4bf into main Nov 15, 2024
2 checks passed
@aphillips aphillips deleted the math-func branch November 15, 2024 19:35

#### Selection

The _function_ `:math` performs selection as described in [Number Selection](#number-selection) below.
Copy link
Member

Choose a reason for hiding this comment

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

The following still bothers me:

If the operand of the expression is an implementation-defined numeric type,
such as the resolved value of an expression with a :number or :integer annotation,
it can include option values.

I think we should replace that by:

If the operand of the expression is an implementation-defined numeric type,
such as the resolved value of an expression with a :number or :integer annotation,
it includes their options.

I really don't think we want the following two to differ in selection behavior, formatting, etc.

.local $y = {|3| :number maximumSignificantDigits=2}
.local $x = {$y :math subtract=1}
.local $x = {|2| :number maximumSignificantDigits=2}

So I'd recommend that the resolved value of :math inherit both the 'datatype' and the options from its operand.

@mihnita
Copy link
Collaborator

mihnita commented Nov 15, 2024

I'm going to merge this. @mihnita (or anyone else) If you have concerns about the resulting text, please raise PRs or issues.

I commented 3 days ago, with no answer.

But I would like to see how the most common use case looks like. The one in my example or (more complicated) the one in issue #701, which this PR tries to solve.

For now I don't see how this is solving the problem.

So yes, I DO have concerns. Unanswered.


It also makes things less intuitive. Select on math is not something people do.
One would expect math to take one or more numeric values, do some operation(s), and return some more numeric values.
Not to format, and not to select.

If this proposal makes :math format and select, it is not much different than :integer with offset.

It also hijacks a very good name (math) that would probably be better use later for, you know, math things?

@macchiati
Copy link
Member

macchiati commented Nov 15, 2024 via email

@macchiati
Copy link
Member

macchiati commented Nov 15, 2024 via email

@aphillips
Copy link
Member

@macchiati

I think we should replace that by:

If the operand of the expression is an implementation-defined numeric type,
such as the resolved value of an expression with a :number or :integer annotation,
it includes their options.

The reason it does not say "their options" is that some options are not passed through. This isn't necessarily the case for :number or :integer currently, but it might in the future. Notice that the resolved value examples are "such as", but might contain e.g. :unit or :currency or some other numeric type expression. Only the passed options are replicated down.

Otherwise fully agree with your observations.

@aphillips
Copy link
Member

... and a second later it occurs to me to point out that :math's options are not passed.

@mihnita
Copy link
Collaborator

mihnita commented Nov 15, 2024

Mihai, I view :math as basically producing an altered version of a numeric
resolved value
, and thus having the same "features" (data type and
options) as its operand.

But then what's the difference between this and :integer with offset?
It does the exact same things.


I still want to see an example of how this looks like for the most common use case.


Just curious, why was this merged? Accident?

@macchiati
Copy link
Member

macchiati commented Nov 15, 2024 via email

@aphillips
Copy link
Member

@mihnita I apologize. I didn't mean to overlook your comment.

I merged this because I didn't see further commentary and thought we'd agreed to pursue this in the WG call. I'm trying to tie up loose ends before this coming week's call.

There are three possibilities here, I think. I'm sympathetic to not making :math a formatter for the reason you cite. The options seem to be:

  • Neither a selector nor a formatter. This would make :math a purely "transform" declaration and not valid to use in a pattern. This has a certain appeal. However, it would require care when doing declarations:
.input {$x :integer}
.local $y = {$x :math subtract=1}
.local $z = {$y :integer}
.match $z * {{{$z} is required because selecting on {$y} is an error}}
  • Only a selector. This fixes the double-declaration, but requires annotation in a pattern.
.input {$x :integer}
.local $y = {$x :math subtract=1}
.match $y
* {{{$y :integer} works but {$y} produces fallback}}
  • Both, as now. This produces the problems you enumerated, but makes the resolved value of :math behave normally otherwise.

As I said, arguments can be made for each of these. I have added this to the agenda for Monday.

@mihnita
Copy link
Collaborator

mihnita commented Nov 15, 2024

I merged this because I didn't see further commentary

Yes, but you announced "I'm going to merge this." 52 minutes ago

Mark answered 51 minutes ago, and I answered 51 minutes ago.

Why even ask if there is no time allowed for answers?

I merged this because I didn't see further commentary

I didn't see a point to comment more if a 3 days comment was unaddressed?

I have added this to the agenda for Monday.

Can someone have some example of what the equivalent of this MF1 looks like?

{guestCount, plural, offset:2
     =0 {No guest}}
     =1 {{$guest1}}
     =2 {{$guest1} and {$guest2}}
    one {{$guest1}, {$guest2}, and # other guest.}
  other {{$guest1}, {$guest2}, and # other guests.}
}

Because that is what issue #701 is about.

So that we know what we will discuss on Monday?

@aphillips
Copy link
Member

.input {$guestCount :integer}
.local $others = {$guestCount :math subtract=2}
.match $guestCount $others
0 * {{No guests}}
1 * {{guest {$guest1}}}
2 * {{guests {$guest1} and {$guest2}}}
* one {{guests {$guest1}, {$guest2}, and {$others} more guest}}
* * {{... {$others} more guests}}

Double selectors are needed because you need plural handling for the back part of the list (imagine Polish).

Note that I'm working on a phone so the example is ugly.

@eemeli
Copy link
Collaborator Author

eemeli commented Nov 15, 2024

@mihnita In addition to the example given by @aphillips above, the merged text also includes this example:

> This function is useful for selection and formatting of values that
> differ from the input value by a specified amount.
> For example, it can be used in a message such as this:
> ```
> .input {$like_count :integer}
> .local $others_count = {$like_count :math subtract=1}
> .match $like_count $others_count
> 0 * {{Your post has no likes.}}
> 1 * {{{$name} liked your post.}}
> * 1 {{{$name} and one other person liked your post.}}
> * * {{{$name} and {$others_count} other people liked your post.}}
> ```

@mihnita
Copy link
Collaborator

mihnita commented Nov 15, 2024

Double selectors are needed because you need plural handling for the back part of the list (imagine Polish).

Got you.

So why can't this be done with :integer and offset?
Why do we need this math thing?

Functionality similar to MF1 is implementable without problems with :number.

See it here:
https://github.com/unicode-org/icu/blob/06a23f8d37ce0c3a985bb676cbc05eb7bd5f3573/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MessageFormat2Test.java#L334

Submitted 2 years ago:
https://github.com/unicode-org/icu/pull/2170/files#diff-d51d5c796bff35d045a8d917f5bcef8f9a1f353184125c903003c4c0dbb14d99R333

@eemeli
Copy link
Collaborator Author

eemeli commented Nov 15, 2024

So why can't this be done with :integer and offset?
Why do we need this math thing?

I presented some arguments originally here: #926 (comment)

@mihnita
Copy link
Collaborator

mihnita commented Nov 16, 2024 via email

eemeli added a commit to messageformat/messageformat that referenced this pull request Nov 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LDML46.1 MF2.0 Draft Candidate registry Issue pertains to the function registry
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEEDBACK] add :number offset option
4 participants