Coder Social home page Coder Social logo

proposal-intl-numberformat-v3's Introduction

ECMA-402 Proposal: Intl.NumberFormat V3

Status: Stage 3 (as of July 2021)

Intl.NumberFormat was first added in the initial Intl specification. More recently, the ECMA-402 Proposal Unified Intl.NumberFormat added several new key features. This proposal, which I'm calling "Intl.NumberFormat V3", is one more batch of features that have been shown to be important to clients of this API.

Motivation

In ECMA-402, we receive dozens of feature requests each year. When forming this proposal, the author (sffc) considered every feature request relating to Intl.NumberFormat and put them up against the following criteria:

  1. The feature must have multiple stakeholders.
  2. The feature must have robust prior art, e.g., in CLDR, ICU, or Unicode.
  3. The feature must be difficult to implement in user land (such as a locale data dependency).

All parts of this proposal meet that bar, and furthermore, the author's intent is that all Intl.NumberFormat feature requests meeting that bar are part of this proposal.

Features

formatRange (ECMA-402 #393)

This piece is modeled off of the Intl.DateTimeFormat.prototype.formatRange proposal. It involves adding a new function .formatRange() to the Intl.NumberFormat prototype, in large part following the semantics introduced by .formatRange() in Intl.DateTimeFormat.

const nf = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "CHF",
  maximumFractionDigits: 0,
});
nf.formatRange(3, 5);  // "CHF 3–5"

Peer methods will also be added:

  • Intl.NumberFormat.prototype.formatRangeToParts
  • Intl.PluralRules.prototype.selectRange (#16)

For example:

const pl = new Intl.PluralRules("sl");
pl.selectRange(102, 201);  // "few"

The formatToParts semantics from Intl.DateTimeFormat will be adopted here: parts will gain a source property that will be either "shared", "startRange", or "endRange".

const nf = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "GBP",
  currencyDisplay: "code",
  maximumFractionDigits: 0,
});
nf.formatRangeToParts(3, 5);
/*
[
  {type: "currency", value: "GBP ", source: "shared"}
  {type: "integer", value: "3", source: "startRange"}
  {type: "literal", value: "–", source: "shared"}
  {type: "integer", value: "5", source: "endRange"}
]
*/

When both sides of the range resolve to the same value after rounding, an approximately sign will be added. The approximately sign is appended to the number in addition to a minus or plus sign, as applicable. (#10, #11, #13)

const nf = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "EUR",
  maximumFractionDigits: 0,
});
nf.formatRange(2.9, 3.1);  // "~€3"

const nf = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "EUR",
  signDisplay: "always",
});
nf.formatRange(2.999, 3.001);  // "~+€3.00"

If the second argument is less than the first argument, or if either is NaN, an error is thrown. (#12)

const nf = new Intl.NumberFormat("en-US");
nf.formatRange(500, 1/0);  // "500–∞"
nf.formatRange(500, 0/0);  // RangeError
nf.formatRange(500, 0);  // RangeError

Feature Detection

if (Intl.NumberFormat.prototype.formatRange) {
  // feature is available
}

Grouping Enum (ECMA-402 #367)

Main Issue: #3

Currently, Intl.NumberFormat accepts a { useGrouping } option, which accepts a boolean value. However, as reported in the bug thread, there are several options users may want when specifying grouping. This proposal is to make the following be valid inputs to { useGrouping }:

  • false: do not display grouping separators
  • "min2": display grouping separators when there are at least 2 digits in a group; for example, "1000" (first group too small) and "10,000" (now there are at least 2 digits in that group). (Bikeshed: #23)
  • "auto" (default): display grouping separators based on the locale preference, which may also be dependent on the currency. Most locales prefer to use grouping separators.
  • "always": display grouping separators even if the locale prefers otherwise.
  • true: alias for "always"
  • undefined (default): alias for "auto"

In resolvedOptions, either false or one of the three strings will be returned. This is an observable behavior change, because currently only the booleans true and false are returned.

Feature Detection

if (new Intl.NumberFormat("und").resolvedOptions().useGrouping === "auto") {
  // feature is available
}

New Rounding/Precision Options (ECMA-402 #286)

Main Issue: #8

Additional Context: Unified NumberFormat #9

The following additional options are proposed to the Intl.NumberFormat options bag to control rounding behavior:

  • roundingPriority = a string set to either "auto", "morePrecision", or "lessPrecision" (details below)
  • roundingIncrement = a Number in the following list: « 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000 »
    • Nickel rounding: { minimumFractionDigits: 2, maximumFractionDigits: 2, roundingIncrement: 5 }
    • Dime rounding: { minimumFractionDigits: 2, maximumFractionDigits: 2, roundingIncrement: 10 }
  • trailingZeroDisplay = a string expressing the strategy for displaying trailing zeros on whole numbers:
    • "auto" = current behavior. Keep trailing zeros according to minimumFractionDigits and minimumSignificantDigits.
    • "stripIfInteger" = same as "auto", but remove the fraction digits if they are all zero.

roundingIncrement cannot be mixed with significant digits rounding or any setting of roundingPriority other than "auto".

Rounding Priority

Currently, Intl.NumberFormat allows for two rounding strategies: min/max fraction digits, or min/max significant digits. Currently, if both min/max fraction digits and min/max significant digits are both specified, significant digit settings take priority and fraction digit settings are ignored.

The new option roundingPriority specifies two new strategies to resolve mixed fraction digits and significant digits settings. To best express the new strategies, consider the following option bag:

{
    maximumFractionDigits: 2,
    maximumSignificantDigits: 2
}

The above options should be interpreted to mean:

  1. Round the number at the hundredths place
  2. Round the number after the second significant digit

Now, consider the number "4.321". maximumFractionDigits wants to round at the hundredths place, producing "4.32". However, maximumSignificantDigits wants to round after two significant digits, producing "4.3". We therefore have a conflict.

The new setting roundingPriority offers a hint on how to resolve this conflict. There are three options:

  1. roundingPriority: "auto" means that significant digits always win a conflict.
  2. roundingPriority: "morePrecision" means that the result with more precision wins a conflict.
  3. roundingPriority: "lessPrecision" means that the result with less precision wins a conflict.

The whole result is taken atomically, including the trailing zeros. For example:

{
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
    minimumSignificantDigits: 2,
    maximumSignificantDigits: 6
}

Consider the input number "1". minimumFractionDigits wants to retain trailing zeros up to the hundredths place, producing "1.00", whereas minimumSignificantDigits wants to retain only as many as are required to render two significant digits, producing "1.0". However, since maximumSignificantDigits has more precision (rounding to 10^-5, rather than 10^-2 for fraction precision), the resolved answer is "1.0".

Feature Detection

if (new Intl.NumberFormat("und").resolvedOptions().roundingPriority) {
  // feature is available
}

Interpret Strings as Decimals (ECMA-402 #334)

The format() method currently accepts a Number or a BigInt, and strings are interpreted as Numbers. This part proposes redefining strings to be represented as decimals instead of Numbers.

const nf = new Intl.NumberFormat("en-US");
const string = "987654321987654321";
nf.format(string);
// Current:  "987,654,321,987,654,300"
// Proposed: "987,654,321,987,654,321"

A common use case is to store currency amounts as a BigInt of a small unit, like cents. This API can be used to retain full precision of the BigInt:

const nf = new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "EUR",
});
const bi = 1000000000000000110000n;
nf.format(bi + "E-6");
// Current:  "€1,000,000,000,000,000.10"
// Proposed: "€1,000,000,000,000,000.11"

The general syntax for decimal strings, essentially #.#E#, is a widely understood format in computing. The specific version we use is the ECMA-262 StringNumericLiteral grammar, which also allows non-base-10 numbers like hexadecimal and binary.

Arbitrary-precision decimal strings are intended to be used as pass-through for formatting. The champions do not intend for strings to become the de-facto standard for numeric computations in ECMAScript. For a general-purpose arbitrary-precision decimal type, see the Decimal proposal.

Feature Detection

if (new Intl.NumberFormat("und").format("11111111111111111112").indexOf("2") !== -1) {
  // feature is available
}

Rounding Modes (ECMA-402 #419)

Main Issue: #7

Intl.NumberFormat always performs "half-up" rounding (for example, if you have 2.5, it gets rounded to 3). However, we understand that there are users and use cases that would benefit from exposing more options for rounding modes.

The list of rounding modes is proposed to be:

  1. ceil (toward +∞)
  2. floor (toward -∞)
  3. expand (away from 0)
  4. trunc (toward 0)
  5. halfCeil (ties toward +∞)
  6. halfFloor (ties toward -∞)
  7. halfExpand (ties away from 0; current behavior; default)
  8. halfTrunc (ties toward 0)
  9. halfEven (ties toward the value with even cardinality)

The behavior of these modes will reflect the ICU user guide, where "expand" maps to ICU "UP" and "trunc" maps to ICU "DOWN".

Rounding does not look at or change the sign bit of the number. Therefore, -0 and 0 are equivalent for the purposes of rounding. (#21)

Feature Detection

if (new Intl.NumberFormat("und").resolvedOptions().roundingMode) {
  // feature is available
}

Sign Display Negative

Main Issue: #17

A new option signDisplay: "negative" is proposed, according to feedback from clients. The new option will behave like "auto" except that the sign will not be shown on negative zero.

var nf = new Intl.NumberFormat("en", {
  signDisplay: "negative"
});
nf.format(-1.0);  // -1
nf.format(-0.0);  // 0  (note: "auto" produces "-0" here)
nf.format(0.0);   // 0
nf.format(1.0);   // 1  (note: "exceptZero" produces "+1" here)

Feature Detection

try {
  new Intl.NumberFormat("und", { signDisplay: "negative" });
  // feature is available
} catch(e) {
  // feature is NOT available
}

Implementation Status

Shipped in:

  • Safari 15.4
  • Chrome 106
  • Firefox 93
A V8 prototype of the latest spec text can be found in https://chromium-review.googlesource.com/c/v8/v8/+/2336146 w/ the flag --harmony_intl_number_format_v3

proposal-intl-numberformat-v3's People

Contributors

12wrigja avatar chicoxyzzy avatar frankyftang avatar gibson042 avatar idanho avatar jugglinmike avatar littledan avatar ljharb avatar mikesamuel avatar ptomato avatar romulocintra avatar sffc avatar trflynn89 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proposal-intl-numberformat-v3's Issues

Add approximately pattern

Given https://unicode-org.atlassian.net/browse/CLDR-11431, the following is how we could add the approximately pattern to Intl.NumberFormat:

new Intl.NumberFormat({
  signDisplay: "approximately"
});

Expected outputs:

Input en-US result with signDisplay: "approximately"
5 ~5
0 ~0
-0 -0
-5 -5

Alternatively, we could throw RangeError on the negative numbers, since the behavior with them is not well-defined in CLDR.

Related: #6

Should ToIntlMathematicalValue accept non-decimal number strings?

As currently written, ToIntlMathematicalValue accepts non-decimal number strings like for example "0x10", via StringNumericLiteral -> StrNumericLiteral -> NonDecimalIntegerLiteral. Is this intentional? The explainer only mentions decimals, so I just want to make sure supporting non-decimal number strings is expected.

Investigate NaN in spec

From @FrankYFTang:


Also in the README.md it said

Ranges to infinity are supported, but if either value is NaN, an error is thrown. (#12)

nf.formatRange(500, 0/0); // RangeError

But I have a hard time figuring out where in the spec to throw the NaN now.
I don't think ToIntlMathematicalValue will throw if the value is NaN right?
https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/diff.html#sec-partitionnumberrangepattern

If x is a non-finite Number or y is is a non-finite Number, throw a RangeError exception.

What is the definition of "non-finite Number" ?
Is NaN a "non-finite Number" ? I cannot tell
https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/diff.html#sec-partitionnumberpattern
surely won't throw if NaN because

If x is NaN, then
Let n be an implementation- and locale-dependent (ILD) String value indicating the NaN value.

Investigate Symbols

From @FrankYFTang:


is that true, with your change
passing

Symbol(12)
[]
{}
as input to format(), formatToParts() or formatRange()
will throw TypeError

type/value of resolvedOptions().useGrouping

The type of useGrouping used to be a boolean but this proposal change it to accept true, false, "min2", "auto", "always".
How should we set the resolvedOptions().useGrouping?

CollapseNumberRange references the resulting string which should be a resulting list

In the prose describing CollapseNumberRange, the last line is:

The algorithm is implementation dependent, but it must not introduce ambiguity: the resulting string after modification should be unique for all pairs x and y.

It should instead be

The algorithm is implementation dependent, but it must not introduce ambiguity: the resulting list after modification should be unique for all pairs x and y.

https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/diff.html#sec-collapsenumberrange

Add unit attribute to the formatToParts output to match Intl.RelativeTimeFormat

let nf = new Intl.NumberFormat("en", {style: "unit", unit: "day"})
nf.format(3.4)
> "3.4 days"
nf.formatToParts(3.4)
>0: {type: "integer", value: "3"}
1: {type: "decimal", value: "."}
2: {type: "fraction", value: "4"}
3: {type: "literal", value: " "}
4: {type: "unit", value: "days"}

let rf = new Intl.RelativeTimeFormat("en")
rf.format(3.4, "day")
> "in 3.4 days"
rf.formatToParts(3.4, "day")
> 0: {type: "literal", value: "in "}
1: {type: "integer", value: "3", unit: "day"}
2: {type: "decimal", value: ".", unit: "day"}
3: {type: "fraction", value: "4", unit: "day"}
4: {type: "literal", value: " days"}

Notice the output from the Intl.RelativeTimeFormat has the unit: "day" in the integer/decimal/fraction
Should we also make the Intl.NumberFormat to output that ?

I think we should change the spec in NumberFormat v3 to make Intl.NumberFormat output

nf.formatToParts(3.4)
>0: {type: "integer", value: "3", unit: "day"}
1: {type: "decimal", value: ".", unit: "day"}
2: {type: "fraction", value: "4", unit: "day"}
3: {type: "literal", value: " "}
4: {type: "unit", value: "days"}

There is a second issue of the 4: {type: "literal", value: " days"} from the Intl.RelativeTimeFormat, which I think we should discuss in a ECMA402 issue.

Does undefined alias to auto include compact notation?

Right now, this proposal specifies that undefined aliases to "auto".
Does this also include "compact" notation, considering ICU defaults it to "min2" for compact notation so that true equates to 'min2" for compact notation in the current Intl.NumberFormat?

Add paragraph explicitly stating the motivation for this proposal

Suggestion to add a paragraph such as the following to the README:

In ECMA-402, we receive dozens of feature requests each year. When forming this proposal, the author (Shane F. Carr) considered every feature request relating to Intl.NumberFormat and put them up against the following criteria:

  1. The feature must have multiple stakeholders.
  2. The feature must have robust prior art, e.g., in CLDR, ICU, or Unicode.
  3. The feature must be difficult to implement in user land (such as a locale data dependency).

All parts of this proposal meet that bar, and furthermore, the author's intent is that all Intl.NumberFormat feature requests meeting that bar are part of this proposal.

@zbraniecki SGTY?

Improve GetStringOrBooleanOption

Currently, https://tc39.es/proposal-intl-numberformat-v3/out/negotiation/diff.html#sec-getoption
(notice the section ID should be changed from #sec-getoption to #sec-getstringorbooleanoption for GetStringOrBooleanOption instead of the one for the GetOption)
have

1.2.12 GetStringOrBooleanOption ( options, property, values, trueValue, fallback )
The abstract operation GetStringOrBooleanOption extracts the value of the property named
property from the provided options object. If the value is undefined, the operation returns
fallback. If the value is true, the operation returns trueValue. If the value is falsy,
the operation returns false. Otherwise, the operation converts the value to a String,
checks whether it is one of a List of allowed values (which must not be undefined),
and returns the stringified value.

  1. Let value be ? Get(options, property).
  2. If value is undefined, then return fallback.
  3. If value is true, then return trueValue.
  4. Let valueBoolean be ToBoolean(value).
  5. If valueBoolean is false, then return valueBoolean.
  6. Let value be ? ToString(value).
  7. If values does not contain an element equal to value, throw a RangeError exception.
  8. Return value.

I suggest we change it to the following to make it more generalized:

1.2.12 GetStringOrBooleanOption ( options, property, values, ^falseValue,^ trueValue, fallback )
The abstract operation GetStringOrBooleanOption extracts the value of the property named
property from the provided options object. If the value is undefined, the operation returns
fallback. If the value is true, the operation returns trueValue. If the value is false,
the operation returns ^falseValue^. Otherwise, the operation converts the value to a String,
checks whether it is one of a List of allowed values (which must not be undefined),
and returns the stringified value.

  1. Let value be ? Get(options, property).
  2. If value is undefined, then return fallback.
  3. If value is true, then return trueValue.
  4. If value is false, then return ^falseValue^.
  5. Let value be ? ToString(value).
  6. If values does not contain an element equal to value, throw a RangeError exception.
  7. Return value.

@sffc

Move scale to the format method?

The main use case that's interesting for scale is when you have a BigInt of a small unit, like currency micros, and you want to divide it by some constant when formatting.

This raises the question: perhaps, since the scale is assumed to be more related to the number than the formatter settings, that scale should go into the format method. For example:

const nf = new Intl.NumberFormat("fr-FR", {
    style: "currency",
    currency: "EUR",
});
const micros = 1000000n;

// Version 1:
nf.format(micros, { scale: -6 });

// Version 2:
nf.format({
    number: micros,
    scale: -6
});

Improve readability of SetNumberFormatDigitOptions

https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/diff.html#sec-setnfdigitoptions

I suggest we change
15. If hasSd or roundingPriority is not "auto", set needSd to true; else, set needSd to false.
to
15. If hasSd is true or roundingPriority is not "auto", then set needSd to true; else, set needSd to false.

I suggest we change
16. If ( not hasSd and notation is not "compact" ) or roundingPriority is not "auto", set needFd to true; else, set needFd to false.
to
16. set needFd to false.
17. If hasSd is false and notation is not "compact", then set needFd to true.
18. if roundingPriority is not "auto", then set needFd to true.

ICU formatRange output inconsistent with README

From @FrankYFTang:


https://github.com/tc39/proposal-intl-numberformat-v3#formatrange-ecma-402-393

const nf = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "EUR",
maximumFractionDigits: 0,
});
nf.formatRangeToParts(3, 5);
/*
[
{type: "currency", value: "€", source: "startRange"}
{type: "integer", value: "3", source: "startRange"}
{type: "literal", value: "–", source: "shared"}
{type: "integer", value: "5", source: "endRange"}
]
*/
so nf.formatRange(3,5)
should give us "€3 – 5"

but in my prototype now, I got "€3 – €5" and
nf.formatRangeToParts(3, 5)
return
[{type: "currency", value: "€", source: "startRange"},
{type: "integer", value: "3", source: "startRange"},
{type: "literal", value: " – ", source: "shared"},
{type: "currency", value: "€", source: "endRange"},
{type: "integer", value: "5", source: "endRange"}]

Any idea what I need to do to make ICU return "€3 – 5" instead?

Also, if it return "€3 – 5", then the formatRangeToPart should use "shared" for "€" instead, as

[
{type: "currency", value: "€", source: "shared"}
{type: "integer", value: "3", source: "startRange"}
{type: "literal", value: "–", source: "shared"}
{type: "integer", value: "5", source: "endRange"}
]
right?

SetNumberFormatDigitOptions uses non-ECMASpeak syntax

From https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/diff.html#sec-setnfdigitoptions:

If ( not hasSd and notation is not "compact" ) or roundingPriority is not "auto", set needFd to true; else, set needFd to false.

As mentioned in the other ECMA-402 PR, we need to explicitly compare against boolean values. And also parenthesised expressions aren't ECMASpeak syntax. Two possible alternatives are either using ", or if" to separate the two conditions:

If hasSd is false and notation is not "compact", or if roundingPriority is not "auto", set needFd to true; else, set needFd to false.

Or to use separate if-clauses:

If hasSd is false and notation is not "compact", set needFd to true.
Else if roundingPriority is not "auto", set needFd to true.
Else, set needFd to false.

What grouping strategies to include?

The proposal currently includes the following 4 grouping strategies, based on the list from ICU:

  • "never" (false): do not display grouping separators.
  • "min2": display grouping separators when there are at least 2 digits in a group; for example, "1000" (first group too small) and "10,000" (now there are at least 2 digits in that group).
  • "auto" (default): display grouping separators based on the locale preference, which may also be dependent on the currency. Most locales prefer to use grouping separators.
  • "always" (true): display grouping separators even if the locale prefers otherwise.

@jswalden asked if there could be additional options added. Could you clarify?

What settings to include for range formatting?

ICU4J has a number of settings for range formatting:

https://unicode-org.github.io/icu-docs/apidoc/released/icu4j/com/ibm/icu/number/NumberRangeFormatterSettings.html

collapse

Sets the aggressiveness of "collapsing" fields across the range separator. Possible values:

  • ALL: "3-5K miles"
  • UNIT: "3K - 5K miles"
  • NONE: "3K miles - 5K miles"
  • AUTO: usually UNIT or NONE, depending on the locale and formatter settings

The default value is AUTO.

identityFallback

Sets the behavior when the two sides of the range are the same. This could happen if the same two numbers are passed to the formatRange function, or if different numbers are passed to the function but they become the same after rounding rules are applied. Possible values:

  • SINGLE_VALUE: "5 miles"
  • APPROXIMATELY_OR_SINGLE_VALUE: "~5 miles" or "5 miles", depending on whether the number was the same before rounding was applied
  • APPROXIMATELY: "~5 miles"
  • RANGE: "5-5 miles" (with collapse=UNIT)

The default value is APPROXIMATELY.

Do we want to add those options to ECMA-402, or just stick with the defaults?

@romulocintra

signDisplay with formatRange

What should happen here?

const nf = new Intl.NumberFormat("en", {
  signDisplay: "always"
});

console.log(nf.formatRange(50, 70));
console.log(nf.formatRange(50, 50));

Options:

Description 50 - 70 50 - 50
Always obey signDisplay +50–70 +50
Obey on identity fallback 50–70 +50
Ignore 50–70 50

CLDR does not clearly specify this case.

If we picked the second option (obey only on identity fallback), it would work nicely with #6 and #10, to let you toggle back and forth between the two main options for identityFallback.

PluralRuleSelectRange prose could be more concrete

The arguments are formatted a bit strangely -- it makes it seem like this is conditional on four arguments, not that the four arguments are required. In addition, we are not specifying the types here, as we do normally for AOs. We might want to make the prose more specific (it is a bit hand wavy right now). Finally, Doesn’t specify what it is returning.

A question from @ryzokuken: Why does PluralRulesSelectRange not have a ToParts?

Case convention of rounding modes

From #7: It's interesting to see the case conventions of halfEven instead of half-even. This matches our recent decisions. Unfortunately, now we're getting into an area where this convention potentially "infects" or conflicts with the other convention, where it would be half-even: I was hoping that Decimal would use rounding modes that match Intl. Do we want to extend the camelCase string convention here?

Take care about `Number.prototype.toFixed`

Currently, Number.prototype.toFixed cannot specify a different rounding mode, and hope that it can be taken into consideration in this proposal since that it has brought out different kinds of rounding modes in JavaScript.

(1.02).toFixed(1); // => "1.0"
(1.05).toFixed(1); // => "1.1" round by default

// If we can specify types:
(1.05).toFixed(1, 'floor'); // => "1.0"
(1.02).toFixed(1, 'ceiling'); // => "1.1"

Range with NaN, Infinity

I think we should probably forbid NaN and Infinity from taking part in a number range format. What would be your expected behavior in these cases?

new Intl.NumberFormat().formatRange(10, NaN);
new Intl.NumberFormat().formatRange(10, Infinity);

The first case makes no sense. The second case could be an interesting application of the "greater than" pattern, but that's not yet supported in ICU.

For now, better to throw a RangeError. We can remove that restriction later if we ever needed to.

Rounding priority not supported for minimumFractionDigits/minimumSignificantDigits in ICU and the spec

From https://github.com/tc39/proposal-intl-numberformat-v3#rounding-priority:

This resolution algorithm applies separately between the maximum digits settings and the minimum digits settings. So, for example, suppose you had

{
   minimumFractionDigits: 2,
   minimumSignificantDigits: 2
}

Consider the input number "1". minimumFractionDigits wants to retain trailing zeros up to the hundredths place, producing "1.00", whereas minimumSignificantDigits wants to retain only as many as are required to render two significant digits, producing "1.0". We again have a conflict, and the conflict is resolved in the same way.

ICU doesn't support this conflict resolution. From https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/unumberformatter_8h.html#ae481df8b480671a6affec8af37491dd5:

Conflicting minimum fraction and significant digits are always resolved in the direction that results in more trailing zeros.

And the spec seems to follow ICU's behaviour, because the minimum digits aren't taken into account for [[RoundingMagnitude]].

Rounding Options Puzzle

I have a puzzle which has perplexed me.

Below, I list real-life use cases for how users want to round their numbers in Intl.NumberFormat. I am trying to figure out some set of options that is capable of expressing these various different rounding strategies.

Compact Notation Rounding

Input Style 1 Style 2 Style 3
1,234,000 1234K 1234K 1234K
123,400 123K 123K 123K
12,340 12K 12K 12.3K
1,234 1.2K 1.2K 1.23K
1,034 1K 1.0K 1.03K
.1034 .1 .10 .103
.1234 .12 .12 .123

English Descriptions

  • Style 1: When there are 2 or more digits before the decimal separator, round to the nearest integer. Otherwise, round to 2 significant digits. Strip trailing zeros.
  • Style 2: When there are 2 or more digits before the decimal separator, round to the nearest integer. Otherwise, round to 2 significant digits. Retain trailing zeros.
  • Style 3: When there are 3 or more digits before the decimal separator, round to the nearest integer. Otherwise, round to 3 significant digits. Strip trailing zeros.

Thoughts

Style 1 could be expressed as minFrac=0, maxFrac=0, and minSig=2, and when minSig is in conflict with maxFrac, minSig wins, except that we strip trailing zeros. In other words, we could make Style 1 be expressed as:

{
  minimumFractionDigits: 0,
  maximumFractionDigits: 0,
  minimumSignificantDigits: 2
}

However, this approach is not capable of expressing Style 2.

We could have an option like "applyFractionGreaterThanIntDigits", which would mean to use minFrac/maxFrac when there are a certain number of integer digits, and minSig/maxSig when there are fewer. This is not a very pretty option, but it is capable of expressing all three styles:

Option Style 1 Style 2 Style 3
minimumFractionDigits 0 0 0
maximumFractionDigits 0 0 0
minimumSignificantDigits 1 2 1
maximumSignificantDigits 2 2 3
applyFractionGreaterThanIntDigits 2 2 3

Currency Rounding

Input Style 1 Style 2 Style 3 Style 4
1 $1.00 $1 $1.00 $1.00
1.01 $1.01 $1.01 $1.00 $1.00
1.04 $1.04 $1.04 $1.05 $1.00
1.12 $1.12 $1.12 $1.10 $1.10

English Descriptions

  • Style 1: Round with 2 fixed fraction digits.
  • Style 2: Round with 2 fixed fraction digits, but strip trailing zeros if the fraction is zero.
  • Style 3: Nickel rounding: round to the nearest 0.05; display 2 fraction digits.
  • Style 4: Dime rounding: round to the nearest 0.1; display 2 fraction digits.

Thoughts

A simple boolean option "stripFractionWhenEmpty" would solve Style 2.

A simple boolean option "nickelRounding" would solve Style 3.

About Trailing Zeros

Note that minFrac already serves absolutely no purpose other than retaining trailing zeros.

Given that minFrac is really only about retaining trailing zeros, for Style 4, we could let minFrac be greater than maxFrac, but it is weird for a minimum to be greater than a maximum.

Since a lot of the problems in this section, as well as Style 2 in the previous section, involve various different ways of treating trailing zeros, maybe we could introduce a "trailingZeroStyle" option, an enum with several different options that encompass all of the use cases.

Distance Rounding

Input Style 1 Style 2
60 50 yards 50 yards
220 200 yards 200 yards
450 450 yards 450 yards
490 500 yards 500 yards
530 550 yards 500 yards
590 600 yards 600 yards

English Descriptions

  • Style 1: Round to the nearest 50.
  • Style 2: Round to the nearest 50 when below 500, and the nearest 100 when above 500.

Thoughts

Style 1 can be represented by a variant of nickelRounding. We could name the option nickelRoundingMagnitude, and if set, it would override fraction and significant rounding. Alternatively, we could allow minFrac/maxFrac to be less than zero, in which case they express the power of 10 at which you round.

Style 2 involves a cutoff. If we can't figure out how to support it, we could declare it out of scope.

Maybe we should throw the minFrac/maxFrac stuff out the door (keep it for backwards compatibility), and devise a whole new way of thinking about rounding strategies.

@echeran @ryzokuken

Range with negative values

I think we should throw RangeError when either of the inputs to formatRange() is negative.

new Intl.NumberFormat().formatRange(-5, -3);  // RangeError

CLDR's number range data is very limited and does not cover this case in a cohesive way. Better to forbid it, and we can add it later if we need to.

Add morePrecision and lessPrecision RoundingType to "1.5 Properties of Intl.PluralRules Instances"

https://tc39.es/proposal-intl-numberformat-v3/out/pluralrules/diff.html#sec-properties-of-intl-pluralrules-instances

Since InitializePluralRules calls SetNumberFormatDigitOptions it is possible RoundingType in the return value contains morePrecision or lessPrecision

Change

[[RoundingType]] is one of the values fractionDigits or significantDigits, indicating which rounding strategy to use, as discussed in .

to

[[RoundingType]] is one of the values fractionDigits<del> or significantDigits</del><ins>, significantDigits, morePrecision, or  lessPrecision</ins>, indicating which rounding strategy to use, as discussed in .

Potentially confusing behavior of RoundingIncrement with significant digits

The spec currently states that the rounding increment should be applied at the point at which rounding is applied. This works fine for fraction digit rounding:

new Intl.NumberFormat("en", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
    roundingIncrement: 5
}).format(1.03);
// => "1.05"

However, for significant digits rounding, the behavior could be surprising:

const nf = new Intl.NumberFormat("en", {
    minimumSignificantDigits: 2,
    maximumSignificantDigits: 2,
    roundingIncrement: 5
});

nf.format(130);  // 150
nf.format(13);   // 15
nf.format(1.3);  // 1.5

Also, ICU does not support rounding increment combined with significant digits.

I suggest that we throw an exception when roundingIncrement is used in combination with anything that requires significant digits. We can always remove the exception later.

Type of roundingIncrement

roundingIncrement takes an integer that is either 10^n or 5x10^n, where n is an integer >= 0.

What should the type of roundingIncrement be? I was thinking Number, since most other numeric options in Intl are Number. But maybe we want BigInt or the new Intl numerical value we are using in the .format() function.

property name of "TrailingZeroDisplay" should be "trailingZeroDisplay" and the question about the default

https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/diff.html#sec-initializenumberformat
Let trailingZeroDisplay be ? GetOption(options, "TrailingZeroDisplay", "string", « "auto", "stripIfInteger" », "stripIfInteger").

should be
Let trailingZeroDisplay be ? GetOption(options, "trailingZeroDisplay", "string", « "auto", "stripIfInteger" », "stripIfInteger").

also, is it intentional to switch the default to "stripIfInteger"? I cannot find the discussion of such change in #8 nor in https://github.com/tc39/proposal-intl-numberformat-v3

signDisplay: missing option for a reasonable use-case

Use-case:
I'd like to round negative values but I don't want -0 to be formatted into a string with "minus" sign i.e. I prefer "0" instead of "-0"

For example:

const formatter = new Intl.NumberFormat("en", {
  maximumFractionDigits: 1
});

console.log(formatter.format(-0.03));
// Returns: -0
// Desired result: 0

The proposed spec for signDisplay supports these options: auto, always, never and exceptZero. But, none of them seem to produce the output desired for the use-case above - without compromising on something else.

exceptZero comes close, but in addition to removing the minus sign from -0, it also adds a plus sign to positive numbers (as expected) which is not desired for the use-case above.

var formatter = new Intl.NumberFormat("en", {
  maximumFractionDigits: 1,
  signDisplay: "exceptZero"
});

console.log(formatter.format(-0.03));
// Returns the desired result: 0  👍 
// but...

console.log(formatter.format(123));
// Returns: +123 👎 
// Desired result: 123

I found this table containing examples of signDisplay options here: https://github.com/tc39/proposal-unified-intl-numberformat/blob/master/README.md#iii-sign-display. I was hoping to find an option that would produce -1 | 0 | 0 | 1 | NaN, but such an option does not seem to exist.

Am I missing something? What is the recommendation for handling the use-case above?
Thank you.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.