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

[4th Edition] NumberFormat.prototype.formatToParts() #79

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 143 additions & 43 deletions spec/numberformat.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,54 +104,100 @@ <h1>Number Format Functions</h1>
</p>
</emu-clause>

<emu-clause id="sec-formatnumber" aoid="FormatNumber">
<h1>FormatNumber(numberFormat, x)</h1>
<emu-clause id="sec-partitionnumberpattern" aoid="PartitionNumberPattern">
<h1>PartitionNumberPattern(numberFormat, x)</h1>

<p>
When the FormatNumber abstract operation is called with arguments _numberFormat_ (which must be an object initialized as a NumberFormat) and _x_ (which must be a Number value), it returns a String value representing _x_ according to the effective locale and the formatting options of _numberFormat_. This abstract operation functions as follows:
The *PartitionNumberPattern* abstract operation is called with arguments _numberFormat_ (which must be an object initialized as a NumberFormat) and _x_ (which must be a Number value), interprets _x_ as a numeric value, and creates the corresponding parts according to the effective locale and the formatting options of _numberFormat_. The following steps are taken:
</p>

<emu-alg>
1. Let _negative_ be *false*.
1. If the _result_ of isFinite(_x_) is *false*, then
1. If _x_ is *NaN*,
1. Let _n_ be an ILD String value indicating the *NaN* value.
1. Else,
1. Let _n_ be an ILD String value indicating infinity.
1. If _x_ < 0, let _negative_ be *true*.
1. If _x_ is not *NaN* and _x_ < 0, then:
1. Let _x_ be -_x_.
1. Let _pattern_ be the value of _numberFormat_.[[negativePattern]].
1. Else,
1. If _x_ < 0, then
1. Let _negative_ be *true*.
1. Let _x_ be -_x_.
1. If the value of _numberFormat_.[[style]] is *"percent"*, let _x_ be 100 × _x_.
1. If the _numberFormat_.[[minimumSignificantDigits]] and _numberFormat_.[[maximumSignificantDigits]] are present, then
1. Let _n_ be ToRawPrecision(_x_, _numberFormat_.[[minimumSignificantDigits]], _numberFormat_.[[maximumSignificantDigits]]).
1. Let _pattern_ be the value of _numberFormat_.[[positivePattern]].
1. Let _result_ be a new empty List.
1. Let _beginIndex_ be Call(*%StringProto_indexOf%*, _pattern_, *"{"*, *0*).
1. Let _endIndex_ be 0.
1. Let _nextIndex_ be 0.
1. Let _length_ be the number of code units in _pattern_.
1. Repeat while _beginIndex_ is an integer index into _pattern_:
1. Set _endIndex_ to Call(*%StringProto_indexOf%*, _pattern_, *"}"*, _beginIndex_)
1. If _endIndex_ = -1, throw new Error exception.
1. If _beginIndex_ is greater than _nextIndex_, then:
1. Let _literal_ be a substring of _pattern_ from position _nextIndex_, inclusive, to position _beginIndex_, exclusive.
1. Add new part record { [[type]]: *"literal"*, [[value]]: _literal_ } as a new element of the list _result_.
1. Let _p_ be the substring of _pattern_ from position _beginIndex_, exclusive, to position _endIndex_, exclusive.
1. If _p_ is equal *"number"*, then:
1. If _x_ is *NaN*,
1. Let _n_ be an ILD String value indicating the *NaN* value.
1. Add new part record { [[type]]: *"nan"*, [[value]]: _n_ } as a new element of the list _result_.
1. Else if isFinite(_x_) is *false*,
1. Let _n_ be an ILD String value indicating infinity.
1. Add new part record { [[type]]: *"infinity"*, [[value]]: _n_ } as a new element of the list _result_.
1. Else,
1. If the value of _numberFormat_.[[style]] is *"percent"*, let _x_ be 100 × _x_.
1. If the _numberFormat_.[[minimumSignificantDigits]] and _numberFormat_.[[maximumSignificantDigits]] are present, then
1. Let _n_ be ToRawPrecision(_x_, _numberFormat_.[[minimumSignificantDigits]], _numberFormat_.[[maximumSignificantDigits]]).
1. Else,
1. Let _n_ be ToRawFixed(_x_, _numberFormat_.[[minimumIntegerDigits]], _numberFormat_.[[minimumFractionDigits]], _numberFormat_.[[maximumFractionDigits]]).
1. If the value of the _numberFormat_.[[numberingSystem]] matches one of the values in the "Numbering System" column of <emu-xref href="#table-numbering-system-digits"></emu-xref> below, then
1. Let _digits_ be an array whose 10 String valued elements are the UTF-16 string representations of the 10 _digits_ specified in the "Digits" column of the matching row in <emu-xref href="#table-numbering-system-digits"></emu-xref>.
1. Replace each _digit_ in _n_ with the value of _digits_[_digit_].
1. Else use an implementation dependent algorithm to map _n_ to the appropriate representation of _n_ in the given numbering system.
1. Let _decimalSepIndex_ be Call(*%StringProto_indexOf%*, _n_, *"."*, 0).
1. If _decimalSepIndex_ > 0, then:
1. Let _integer_ be the substring of _n_ from position 0, inclusive, to position _decimalSepIndex_, exclusive.
1. Let _fraction_ be the substring of _n_ from position _decimalSepIndex_, exclusive, to the end of _n_.
1. Else:
1. Let _integer_ be _n_.
1. Let _fraction_ be *undefined*.
1. If the value of the _numberFormat_.[[useGrouping]] is *true*,
1. Let _groupSepSymbol_ be the ILND String representing the grouping separator.
1. Let _groups_ be a List whose elements are, in left to right order, the substrings defined by ILND set of locations within the _integer_.
1. Assert: The number of elements in _groups_ List is greater than *0*.
1. Repeat, while _groups_ List is not empty:
1. Remove the first element from _groups_ and let _integerGroup_ be the value of that element.
1. Add new part record { [[type]]: *"integer"*, [[value]]: _integerGroup_ } as a new element of the list _result_.
1. If _groups_ List is not empty, then:
1. Add new part record { [[type]]: *"group"*, [[value]]: _groupSepSymbol_ } as a new element of the list _result_.
1. Else,
1. Add new part record { [[type]]: *"integer"*, [[value]]: _integer_ } as a new element of the list _result_.
1. If _fraction_ is not *undefined*, then:
1. Let _decimalSepSymbol_ be the ILND String representing the decimal separator.
1. Add new part record { [[type]]: *"decimal"*, [[value]]: _decimalSepSymbol_ } as a new element of the list _result_.
1. Add new part record { [[type]]: *"fraction"*, [[value]]: _fraction_ } as a new element of the list _result_.
1. Else if _p_ is equal *"plusSign"*, then:
1. Let _plusSignSymbol_ be the ILND String representing the plus sign.
1. Add new part record { [[type]]: *"plusSign"*, [[value]]: _plusSignSymbol_ } as a new element of the list _result_.
1. Else if _p_ is equal *"minusSign"*, then:
1. Let _minusSignSymbol_ be the ILND String representing the minus sign.
1. Add new part record { [[type]]: *"minusSign"*, [[value]]: _minusSignSymbol_ } as a new element of the list _result_.
1. Else if _p_ is equal *"percentSign"* and _numberFormat_.[[style]] is *"percent"*, then:
1. Let _percentSignSymbol_ be the ILND String representing the percent sign.
1. Add new part record { [[type]]: *"percentSign"*, [[value]]: _percentSignSymbol_ } as a new element of the list _result_.
1. Else if _p_ is equal *"currency"* and _numberFormat_.[[style]] is *"currency"*, then:
1. Let _currency_ be the value of _numberFormat_.[[currency]].
1. Assert: _numberFormat_.[[currencyDisplay]] is *"code"*, *"symbol"* or *"name"*.
1. If _numberFormat_.[[currencyDisplay]] is *"code"*, then
1. Let _cd_ be _currency_.
1. Else if _numberFormat_.[[currencyDisplay]] is *"symbol"*, then
1. Let _cd_ be an ILD string representing _currency_ in short form. If the implementation does not have such a representation of _currency_, use _currency_ itself.
1. Else if _numberFormat_.[[currencyDisplay]] is *"name"*, then
Copy link
Contributor

@caridy caridy May 12, 2016

Choose a reason for hiding this comment

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

there is a missing code-path here... either an assertion that there is always a value for currencyDisplay, or an else with the right value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, thanks. I think an assertion is fine, as we'd already throw a RangeError in InitializeNumberFormat (§11.1.1 step 20):

Let cd be ? GetOption(options, "currencyDisplay", "string", « "code", "symbol", "name" », "symbol").

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or did you mean that one of the valid values (code, symbol or name) should simply be in an else block without checking what it is?

1. Let _cd_ be an ILD string representing _currency_ in long form. If the implementation does not have such a representation of _currency_, then use _currency_ itself.
1. Add new part record { [[type]]: *"currency"*, [[value]]: _cd_ } as a new element of the list _result_.
1. Else,
1. Let _n_ be ToRawFixed(_x_, _numberFormat_.[[minimumIntegerDigits]], _numberFormat_.[[minimumFractionDigits]], _numberFormat_.[[maximumFractionDigits]]).
1. If the value of the _numberFormat_.[[numberingSystem]] matches one of the values in the "Numbering System" column of <emu-xref href="#table-numbering-system-digits"></emu-xref> below, then
1. Let _digits_ be an array whose 10 String valued elements are the UTF-16 string representations of the 10 _digits_ specified in the "Digits" column of the matching row in <emu-xref href="#table-numbering-system-digits"></emu-xref>.
1. Replace each _digit_ in _n_ with the value of _digits_[_digit_].
1. Else use an implementation dependent algorithm to map _n_ to the appropriate representation of _n_ in the given numbering system.
1. If _n_ contains the character *"."*, replace it with an ILND String representing the decimal separator.
1. If the value of the _numberFormat_.[[useGrouping]] is *true*, insert an ILND String representing a grouping separator into an ILND set of locations within the integer part of _n_.
1. If _negative_ is *true*, then
1. Let _result_ be the value of _numberFormat_.[[negativePattern]].
1. Else,
1. Let _result_ be the value of _numberFormat_.[[positivePattern]].
1. Replace the substring *"{number}"* within _result_ with _n_.
1. If the value of the _numberFormat_.[[style]] is *"currency"*, then
1. Let _currency_ be the value of _numberFormat_.[[currency]].
1. If _numberFormat_.[[currencyDisplay]] is *"code"*, then
1. Let _cd_ be _currency_.
1. Else if _numberFormat_.[[currencyDisplay]] is *"symbol"*, then
1. Let _cd_ be an ILD string representing _currency_ in short form. If the implementation does not have such a representation of _currency_, use _currency_ itself.
1. Else if _numberFormat_.[[currencyDisplay]] is *"name"*, then
1. Let _cd_ be an ILD string representing _currency_ in long form. If the implementation does not have such a representation of _currency_, then use _currency_ itself.
1. Replace the substring *"{currency}"* within _result_ with _cd_.
1. Let _literal_ be the substring of _pattern_ from position _beginIndex_, inclusive, to position _endIndex_, inclusive.
1. Add new part record { [[type]]: *"literal"*, [[value]]: _literal_ } as a new element of the list _result_.
1. Set _nextIndex_ to _endIndex_ + 1.
1. Set _beginIndex_ to Call(*%StringProto_indexOf%*, _pattern_, "{", _nextIndex_)
1. If _nextIndex_ is less than _length_, then:
1. Let _literal_ be the substring of _pattern_ from position _nextIndex_, inclusive, to position _length_, exclusive.
1. Add new part record { [[type]]: *"literal"*, [[value]]: _literal_ } as a new element of the list _result_.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should use _index_ instead of _endIndex_ and the substring should be from position _index_, inclusive, to position _length_, exclusive. Otherwise, an empty literal is produced at the end.

1. Return _result_.
</emu-alg>


<emu-table id="table-numbering-system-digits">
<emu-caption>Numbering systems with simple digit mappings</emu-caption>
<table class="real-table">
Expand Down Expand Up @@ -257,10 +303,47 @@ <h1>FormatNumber(numberFormat, x)</h1>
</emu-note>

<emu-note>
It is recommended that implementations use the locale data provided by the Common Locale Data Repository (available at <a href="http://cldr.unicode.org/">http://cldr.unicode.org/</a>).
It is recommended that implementations use the locale provided by the Common Locale Data Repository (available at <a href="http://cldr.unicode.org/">http://cldr.unicode.org/</a>).
</emu-note>
</emu-clause>

<emu-clause id="sec-formatnumber" aoid="FormatNumber">
<h1>FormatNumber(numberFormat, x)</h1>

<p>
The FormatNumber abstract operation is called with arguments _numberFormat_ (which must be an object initialized as a NumberFormat) and _x_ (which must be a Number value), and performs the following steps:
</p>

<emu-alg>
1. Let _parts_ be ? PartitionNumberPattern(_numberFormat_, _x_).
1. Let _result_ be an empty String.
1. For each _part_ in _parts_, do:
1. Set _result_ to a String value produced by concatenating _result_ and _part_.[[value]].
1. Return _result_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-formatnumbertoparts" aoid="FormatNumberToParts">
<h1>FormatNumberToParts(numberFormat, x)</h1>

<p>
The FormatNumberToParts abstract operation is called with arguments _numberFormat_ (which must be an object initialized as a NumberFormat) and _x_ (which must be a Number value), and performs the following steps:
</p>

<emu-alg>
1. Let _parts_ be ? PartitionNumberPattern(_numberFormat_, _x_).
1. Let _result_ be ArrayCreate(0).
1. Let _n_ be 0.
1. For each _part_ in _parts_, do:
1. Let _O_ be ObjectCreate(%ObjectPrototype%).
1. Perform ? CreateDataPropertyOrThrow(_O_, "type", _part_.[[type]]).
1. Perform ? CreateDataPropertyOrThrow(_O_, "value", _part_.[[value]]).
1. Perform ? CreateDataPropertyOrThrow(_result_, ? ToString(_n_), _O_).
1. Increment _n_ by 1.
1. Return _result_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-torawprecision" aoid="ToRawPrecision">
<h1>ToRawPrecision(x, minPrecision, maxPrecision)</h1>

Expand Down Expand Up @@ -409,7 +492,7 @@ <h1>Internal slots</h1>

<ul>
<li>The array that is the value of the "nu" property of any locale property of [[localeData]] must not include the values "native", "traditio", or "finance".</li>
<li>[[localeData]][locale] must have a patterns property for all locale values. The value of this property must be an object, which must have properties with the names of the three number format styles: *"decimal"*, *"percent"*, and *"currency"*. Each of these properties in turn must be an object with the properties positivePattern and negativePattern. The value of these properties must be string values that contain a substring *"{number}"*; the values within the currency property must also contain a substring *"{currency}"*. The pattern strings must not contain any characters in the General Category “Number, decimal digit" as specified by the Unicode Standard.</li>
<li>[[localeData]][locale] must have a patterns property for all locale values. The value of this property must be an object, which must have properties with the names of the three number format styles: *"decimal"*, *"percent"*, and *"currency"*. Each of these properties in turn must be an object with the properties positivePattern and negativePattern. The value of these properties must be string values that must contain the substring *"{number}"* and may contain the substrings *"{plusSign}"*, and *"{minusSign}"*; the values within the percent property must also contain the substring *"{percentSign}"*; the values within the currency property must also contain the substring *"{currency}"*. The pattern strings must not contain any characters in the General Category “Number, decimal digit" as specified by the Unicode Standard.</li>
</ul>

<emu-note>
Expand Down Expand Up @@ -457,16 +540,33 @@ <h1>get Intl.NumberFormat.prototype.format</h1>
<emu-alg>
1. Let _nf_ be *this* value.
1. If Type(_nf_) is not Object, throw a *TypeError* exception.
1. If _nf_ does not have an [[initializedNumberFormat]] internal slot, throw a *TypeError* exception.
1. If _nf_.[[boundFormat]] is *undefined*, then
1. If _nf_ does not have an [[initializedNumberFormat]] internal slot whose value is *true*, throw a *TypeError* exception.
1. If the [[boundFormat]] internal slot of _nf_ is *undefined*, then
1. Let _F_ be a new built-in function object as defined in Number Format Functions (<emu-xref href="#sec-number-format-functions"></emu-xref>).
1. Let _bf_ be BoundFunctionCreate(_F_, _nf_, « »).
1. Perform ! DefinePropertyOrThrow(_bf_, `"length"`, PropertyDescriptor {[[Value]]: 1, [[Writable]]: *false*, [[Enumerable]]: *false*, [[Configurable]]: *true*}).
1. Set _nf_.[[boundFormat]] to bf.
1. Set _nf_.[[boundFormat]] to _bf_.
1. Return _nf_.[[boundFormat]].
</emu-alg>
</emu-clause>

<emu-clause id="sec-intl.numberformat.prototype.formattoparts">
<h1>Intl.NumberFormat.prototype.formatToParts ([ value ])</h1>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Making the value argument to Intl.NumberFormat.prototype.formatToParts optional is consistent with the DateTimeFormat version of the function. But it doesn't seem to me that it should be.

There's a somewhat-understandable default value that can be applied for dates, namely the current date/time. But there's not really a good default value for numbers. You could argue for it being zero, but wouldn't it be more sensible for the user to pass 0 in that case? Or you could do what the current patch seems to do and make the default NaN (probably not by deliberate choice? it's just a consequence of ToNumber(undefined) evaluating to it). But it hardly seems like people will generally want to format NaN into parts. As often as not, NaN appearing somewhere means some earlier step in execution went awry, and now things are in la-la land.

I think you should make the value argument here non-optional by removing the brackets and eliminating the step where "value is not provided". Just let lack-of-argument be filled in with undefined by normal spec semantics. This won't stop users from failing to provide the argument, and if they don't pass the argument, they'll still get dumb NaN-handling. But it'll make Intl.NumberFormat().formatToParts.length have a value consistent with how people are expected to use it, and will overwhelmingly use it.

Copy link
Contributor

Choose a reason for hiding this comment

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

hey @jswalden, IIRC, the decision was based on the idea to align with format: Intl.NumberFormat.prototype.format([value]), which is specified here: https://tc39.github.io/ecma402/#sec-number-format-functions, and specifically, the idea of the progression from format to formatToParts has a way to evolve your ui. If moving from format to formatToParts requires extra logic, then it seems to me like a refactor hazard.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@caridy This wouldn't change any logic -- it would simply change the .length of the function, which wouldn't be consulted by anything using .format directly (because if you know the identity of your callee that way, you wouldn't have any reason to check its .length). If you wanted to be more clear that there's no semantic change in the function's behavior, and that the only change is to the value of its .length, you could have the exact same effect by adding, "The value of the length property of the formatToParts method is 1." just as Intl.Collator.supportedLocalesOf has such language.

Copy link
Contributor

Choose a reason for hiding this comment

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

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 agree with @jswalden here. The current default behavior doesn't make much sense (NaN), so I'm ok with requiring an argument.

Copy link
Contributor

Choose a reason for hiding this comment

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

@zbraniecki I don't think that's what @jswalden is proposing. He is just proposing to set the length to 1, but keeping value as optional.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be nice for value to be truly non-optional, but the ship's sailed on having JS APIs that enforce arity. So I think only that the length should be 1, not 0. Intl.NumberFormat().formatToParts() would act as if NaN were passed, even though it's dumb and doesn't make much sense, because it's how JS APIs generally work, that failure to provide an argument is identical to passing undefined for that argument.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm supportive of length to be 1, and we can probably have a discussion about whether or not we want value to be optional, it is not too late for that!

Copy link
Member

Choose a reason for hiding this comment

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

I guess I wanted @jswalden's opinion to be the one I presented ;) #argumentumadverecundiam


<p>
When the *Intl.NumberFormat.prototype.formatToParts* is called with an optional argument _value_, the following steps are taken:
</p>

<emu-alg>
1. Let _nf_ be *this* value.
1. If Type(_nf_) is not Object, throw a *TypeError* exception.
1. If _nf_ does not have an [[initializedNumberFormat]] internal slot, throw a *TypeError* exception.
1. If _value_ is not provided, let _value_ be *undefined*.
1. Let _x_ be ? ToNumber(_value_).
1. Return ? FormatNumberToParts(_nf_, _x_).
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 think you can just directly return the result of FormatNumberToParts to ECMAScript. FormatNumberToParts returns a List of Records. You have to convert the List of Records into an Array of Objects.

Copy link
Member

Choose a reason for hiding this comment

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

@littledan - this is fixed now

</emu-alg>
</emu-clause>

<emu-clause id="sec-intl.numberformat.prototype.resolvedoptions">
<h1>Intl.NumberFormat.prototype.resolvedOptions ()</h1>

Expand Down