-
Notifications
You must be signed in to change notification settings - Fork 108
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
Normative: Allow users to specify rounding based on cash denominations in common use #839
Conversation
…ws rounding currency values to the number of fractional digits corresponding to the smallest currency denomination in circulation
… rounding to smallest cash denominations for CAD, CHF, DKK
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks basically right! Obviously we need to present this to TG2 and then TG1 and then make sure there's good test and MDN coverage. This is about the biggest I would want a normative PR to be.
Note that the spec has the attribute named |
I haven't formed a position yet on the normative reference to CLDR. Happy to have that discussion. |
Ah, of course! I've edited the comment |
…llowing rounding to smallest cash denominations for CAD, CHF, DKK
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks about right to me; would like input from @gibson042 on the CLDR normative reference and from the rest of TG2 on the option names
1. Let _mnfdDefault_ be _cDigits_. | ||
1. Let _mxfdDefault_ be _cDigits_. | ||
1. Else, | ||
1. Let _defaultRoundingIncrement_ be 1. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Observation: not sure how the spec scopes variables but this Let
is not in the same lexical scope as the code below that reads from it. If it is a problem, you could Let
it be 1 further up and then Set
it to something else when needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine; aliases are implicitly scoped to the containing algorithm: https://tc39.es/ecma262/multipage/notational-conventions.html#sec-algorithm-conventions (emphasis mine)
Once declared, an alias may be referenced in any subsequent steps and must not be referenced from steps prior to the alias's declaration.
@@ -215,6 +219,7 @@ <h1> | |||
1. Else, | |||
1. If IsWellFormedCurrencyCode(_currency_) is *false*, throw a *RangeError* exception. | |||
1. Let _currencyDisplay_ be ? GetOption(_options_, *"currencyDisplay"*, ~string~, « *"code"*, *"symbol"*, *"narrowSymbol"*, *"name"* », *"symbol"*). | |||
1. Let _currencyPrecision_ be ? GetOption(_options_, *"currencyPrecision"*, ~string~, « *"cash"*, *"financial"* », *"financial"*). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a reminder to bikeshed all parts of this: the option name and both string options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering about the difference between "financial" here vs. "accounting" for currencySign
(i.e., should both options instead use the same "accounting" value?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My sense is no, since there's situations where you'd want to use one and not the other. I can easily imagine someone using (for example) a personal finance application wanting debts represented by wrapping them in parentheses, but also not caring about anything smaller than a cent.
Avoiding confusion between the options is one reason why I think "financial" would be a better name than "accounting" for this option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have substantive comments regarding normatively referencing CLDR and (especially) when this new option should affect output, the latter of which highlights what I believe to be problematic behavior in the current specification when style
is "currency" and notation
is not "standard".
@@ -215,6 +219,7 @@ <h1> | |||
1. Else, | |||
1. If IsWellFormedCurrencyCode(_currency_) is *false*, throw a *RangeError* exception. | |||
1. Let _currencyDisplay_ be ? GetOption(_options_, *"currencyDisplay"*, ~string~, « *"code"*, *"symbol"*, *"narrowSymbol"*, *"name"* », *"symbol"*). | |||
1. Let _currencyPrecision_ be ? GetOption(_options_, *"currencyPrecision"*, ~string~, « *"cash"*, *"financial"* », *"financial"*). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering about the difference between "financial" here vs. "accounting" for currencySign
(i.e., should both options instead use the same "accounting" value?).
1. Let _cDigits_ be CurrencyDigits(_currency_). | ||
1. Let _currencyPrecision_ be _numberFormat_.[[CurrencyPrecision]]. | ||
1. Let _cDigits_ be CurrencyDigits(_currency_, _currencyPrecision_). | ||
1. Let _defaultRoundingIncrement_ be CurrencyRoundingIncrement(_currency_, _currencyPrecision_). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's an interaction between {minimum,maximum}FractionDigits and roundingIncrement that I think this is failing to account for by setting defaultRoundingIncrement in ignorance of the former:
const input = 55.555;
for (const maximumFractionDigits of [0, 1, 2]) {
for (const roundingIncrement of [1, 50]) {
const nfOptions = {
style: "currency",
currency: "USD",
maximumFractionDigits,
roundingIncrement,
};
let pattern = String(10 ** -maximumFractionDigits).replace(/[0-9]/g, "#");
const output = new Intl.NumberFormat("en", nfOptions).format(input);
console.log(`${input} as ${pattern} @ increment ${roundingIncrement}: ${output}`);
}
}
/* =>
55.555 as # @ increment 1: $56
55.555 as # @ increment 50: $50
55.555 as #.# @ increment 1: $55.6
55.555 as #.# @ increment 50: $55.0
55.555 as #.## @ increment 1: $55.56
55.555 as #.## @ increment 50: $55.50
*/
Given the above, what would we expect from currencyPrecision: "cash", maximumFractionDigits: 0
vs. currencyPrecision: "cash", maximumFractionDigits: 1
vs. currencyPrecision: "cash", maximumFractionDigits: 2
?
Speaking personally, I would expect currencyPrecision
to always apply at the same absolute position regardless of output precision, which suggests to me that the algorithm needs to account for digits options (and for that matter, notation
as well—we arguably have a preexisting bug where currency-derived fractional digit count defaults are applied even when the decimal point is moved away from its absolute position, as in new Intl.NumberFormat("en", { style: "currency", currency, currencyDisplay: "code", notation: "engineering" }).format(12345).replace(/^.*\s/, "")
resulting in "12E3" for JPY but "12.35E3" for USD).
Put another way, I think I expect currency-derived precision and corresponding rounding to apply only when notation
is "standard" (i.e., not even when the decimal point happens to be in the right place with scientific/engineering/compact notation), and only in that context does it make sense for a new option to further refine the output.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh that's a good point. Great catch. I should have noticed that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, very good catch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of thoughts:
- It seems reasonable to throw if
currencyPrecision: "cash"
is set andnotation
isn't "standard", since cash rounding only really makes sense in standard notation. - If cash rounding only makes sense if
notation
is "standard", this suggests that the non-cash option forcurrencyPrecision
should be something other than "financial" — maybe just "standard"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an issue not only when notation
isn't "standard" but also when a different value was set for minimumFractionDigits or maximumFractionDigits. So the condition when this is used should be exactly the condition when _cDigits_
is used.
I'm kind-of okay with any of the following outcomes:
- Do a complicated check that the setting isn't present with conflicting options
- Ignore the setting if conflicting options are present (this is how we behave when e.g. currencySign is present but style is not currency)
- Define the algorithm similar to how you have it defined here and let people shoot themselves in the foot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gut feelings when I see those potential outcomes worded that way:
3 seems bad (needlessly confusing for users)
1 seems less bad but not in keeping with the rest of the spec
2 seems least bad
on edit: Making sure I understand 2: if minimumFractionDigits or maximumFractionDigits is set, currencyPrecision
is ignored, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concretely the change would be the following in SetNumberFormatDigitOptions:
Undo your change on this line:
1. Let _roundingIncrement_ be ? GetNumberOption(_options_, *"roundingIncrement"*, 1, 5000, 1).
And add the following line:
1. Set _intlObj_.[[MinimumFractionDigits]] to _mnfdDefault_.
1. Set _intlObj_.[[MaximumFractionDigits]] to _mxfdDefault_.
+ 1. Set _intlObj_.[[RoundingIncrement]] to _defaultRoundingIncrement_.
I think that gets the job done?
1. Let _mnfdDefault_ be _cDigits_. | ||
1. Let _mxfdDefault_ be _cDigits_. | ||
1. Else, | ||
1. Let _defaultRoundingIncrement_ be 1. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine; aliases are implicitly scoped to the containing algorithm: https://tc39.es/ecma262/multipage/notational-conventions.html#sec-algorithm-conventions (emphasis mine)
Once declared, an alias may be referenced in any subsequent steps and must not be referenced from steps prior to the alias's declaration.
spec/numberformat.html
Outdated
<emu-alg> | ||
1. Assert: IsWellFormedCurrencyCode(_currency_) is *true*. | ||
1. Assert: _currency_ is equal to the ASCII-uppercase of _currency_. | ||
1. If the Common Locale Data Repository supplemental data pertaining to fractional currency values contains an element with _currency_ as the value of its iso4217 attribute, then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm comfortable with generalizing beyond ISO 4217, but this seems like the kind of data for which an implementation could conceivably have reasons to diverge from CLDR (i.e., superior local knowledge). So I'd rather have the reference be a recommendation rather than a requirement (aligning with other sections of the spec).
…CLDR, allowing rounding to smallest cash denominations for CAD, CHF, DKK
@ben-allen, @romulocintra and I discussed converting this to a proposal that can go through the stage process. |
TG2 notes: https://github.com/tc39/ecma402/blob/main/meetings/notes-2024-01-18.md#support-cash-rounding-835 Conclusion: Add Test262 and docs for the current spec behavior. LGTM: SFC, BAN, FYT |
Closing this PR but there is still work tracked in the issues linked in the OP |
fix #134
fix #835
For some currencies, the number of fractional digits and rounding increment used when dealing with cash values differs from the fractional digits/rounding increments used in financial contexts, with the cash contexts being limited to what's allowed for by the smallest currency denominations in actual circulation. ISO 4217 data uses the values for financial rounding, which is often inappropriate for common usage, while CLDR's currency data has separate attributes for:
This PR allows users to specify a new "currencyPrecision" attribute, which can be set to either "cash" (using CLDR's cash data) or "financial" (using CLDR's financial data, which is almost identical to ISO 4217's values).
This PR makes CLDR normative.
Note 1: I'm not confident about the wording used in CurrencyDigits and the new CurrencyRounding AOs to describe the CLDR data pulled in -- this is to be taken as a best first guess rather than a definitive attempt.
Note 2: Current version shows whether "cash" or "financial" rounding has been set in resolvedOptions, which may not be necessary, as the actual values used for fractional digits and rounding increment are already visible elsewhere in resolvedOptions.