Skip to content

Conversation

@nicolo-ribaudo
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo commented Sep 22, 2025

This is how I think we should handle significant digits. This PR should be though of as on top of #63.

TL;DR:

  • SignificantDigits are not a concept part of the data model. We have ways to convert it to FractionDigits, and we only store FractionDigits.
  • for non-zero real numbers SignificantDigits the conversion happens through (from fractionDigits and significantDigits #54)

    Let $e$ be the unique integer for which $10^e ≤ |value| < 10^{e + 1}$. Then let $significantDigits = e + fractionDigits + 1$.

  • for zero and negative zero, it is $significantDigits = fractionDigits + 1$, matching how Intl already does it, but ensuring that there is at least one significant digit.
  • for NaN and infinities they are both 0

Note that we currently don't have a "on the way out" for it. I think it's just that the current spec text doesn't currently have the .fractionDigits/.significantDigits. Once those getters are added, .significantDigits should be implemented as follows:

1. Return FractionToSignificantDigits(this.[[Value]], this.[[FractionDigits]])

This should match all examples from @waldemarhorwat's table in the significantDigits table from #54. When it comes to zeroes, it does the following:

string fraction digits significant digits output I'd expect when stringifying again
"0" 0 1 "0"
"0.0000" 4 5 "0.0000"
"00.0000" 4 5 "0.0000"
".0000" 4 5 "0.0000"
"0e-3" 3 4 "0.000"
"0e3" -3 1 "0e3"

I'm on the fence whether "0.0000" and ".0000" should be the same. I like that the 0 prefix is optional, but it'd be open to:

  • make .0000 have 4 significant digits
  • make in the input .0000 an error

The 0e-3 case is a bit weird, but it matches that for every non-zero digit x 0.00x and xe-3 are equivalent.

Changing the behavior of .0000 and 0e-3 however requires storing both FractionalDigits and SignificantDigits, and they'd not be anymore two sides of the same coin.

@nicolo-ribaudo nicolo-ribaudo force-pushed the my-idea-of-significant-digits branch 4 times, most recently from dca3b87 to 1c8aa62 Compare September 22, 2025 20:36
@nicolo-ribaudo nicolo-ribaudo force-pushed the my-idea-of-significant-digits branch from 1c8aa62 to 2407c34 Compare September 22, 2025 20:38
Copy link

@waldemarhorwat waldemarhorwat left a comment

Choose a reason for hiding this comment

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

Please also fix up all of the assertions that fractionDigits is nonnegative.

1. Else,
1. Let _e_ be the smallest integer such that _e_ > the base 10 logarithm of abs(_value_).
1. Let _integerDigits_ be 1 + _e_.
1. Assert: _integerDigits_ + _fractionDigits_ > 0.

Choose a reason for hiding this comment

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

This assert can fail for zeroes. That's one of the reasons why I don't think we should compute significantDigits from fractionDigits.

FractionToSignificantDigits is not used anywhere. Just delete it.

@nicolo-ribaudo
Copy link
Member Author

nicolo-ribaudo commented Sep 23, 2025

@waldemarhorwat Thanks for the review! I wrote that FractionToSignificantDigits AO assuming that it's what would be used by the .significantDigits accessor mentioned in the proposal readme. I'd like to have some concrete good semantics we can go with if we don't choose to remove the method.

I updated the zero-handling logic to ensure that when we have a negative number of fraction digits we still have one significant digit. Another option would be to say that 0 always has exactly one significant digit (i.e. the right-most 0), while keeping the "input" part of it as it is currently in this PR (so that it's coherent with Intl).

@nicolo-ribaudo
Copy link
Member Author

After talking with @jessealama, I'm starting to be convinced that 0 should always have 1 significant digit. In "0.000 meters", the only thing that matters is that the last digit is a 0. It's telling that we measured that distance with a tool that has 1mm resolution, and on that tool we got 0 as a result.

However, to align with Intl, we should still accept non-1 significant digits as input, and normalize them. In Intl.NumberFormat, if you ask to format 0 with 3 significant digits you'll get 0.00. We should normalize that to have 2 fraction digits and 1 significant digit:

new Amount("0.00").fractionDigits // 2
new Amount("0.00").significantDigit // 1

new Amount(".00").fractionDigits // 2
new Amount(".00").significantDigit // 1

new Amount("0e-2").fractionDigits // 2
new Amount("0e-2").significantDigit // 1

new Amount("0e2").fractionDigits // -2
new Amount("0e2").significantDigit // 1

new Amount("0").fractionDigits // 0
new Amount("0").significantDigit // 1

new Amount("000").fractionDigits // 0
new Amount("000").significantDigit // 1

new Amount("0", { significantDigits: 3 }).fractionDigits // 2
new Amount("0", { significantDigits: 3 }).significantDigits // 1

new Amount("0", { fractionDigits: 2 }).fractionDigits // 2
new Amount("0", { fractionDigits: 2 }).significantDigits // 1

This would be done by leaving the SignificantToFractionDigits AO introduced by this PR as-is, but changing FractionToSignificantDigits to, in the case of 0, always return 1.

@jessealama
Copy link
Collaborator

relates to #70

@gibson042
Copy link
Member

After talking with @jessealama, I'm starting to be convinced that 0 should always have 1 significant digit.

In Intl.NumberFormat, if you ask to format 0 with 3 significant digits you'll get 0.00. We should normalize that to have 2 fraction digits and 1 significant digit

This does not match any definition of "significant digit" with which I am familiar. The only insignificant digits in an otherwise-unannotated decimal number are leading/placeholder zeros, which as a concept is notably independent of "fraction digit" (i.e., a zero to the right of the decimal point is always a fraction digit but may be significant or insignificant depending upon the digits to its left). See also Wikipedia section Rules to identify significant figures in a number.

The only special behavior necessary with all-zeros input is identifying the most significant digit, and I think the best way to do so is defining it as the leftmost after normalizing leading/missing zeros left of the decimal point to a single zero:

  • new Amount("0.00"): 2 fraction digits, 3 significant digits
  • new Amount(".00"): same (the units-place 0 is implied)
  • new Amount("0", { significantDigits: 3 }): same
  • new Amount("0", { fractionDigits: 2 }): same
  • new Amount("0e-2") [0.00]: 2 fraction digits, 1 significant digit
  • new Amount("0e2") [000]: no fraction digits (but fractionDigits/scale/etc. can be e.g. -2 to indicate the boundary of significance), 1 significant digit
  • new Amount("0"): no fraction digits, 1 significant digit
  • new Amount("000"): same (leading zeros are not significant)

@waldemarhorwat
Copy link

There are still places throughout the code that either assume or assert that fractionDigits is nonnegative. Please go through them and fix them up.

@nicolo-ribaudo
Copy link
Member Author

nicolo-ribaudo commented Sep 23, 2025

@gibson042 Those rules do not work with numbers whose value is 0. We need to pick which zero is the one that matters. In 0.00:

  • if the one that matters is the left-most one, then it's like 1.00 and thus it has 3 significant digits
  • if the one that matters is the right-most one, then it's like 0.01 and thus it has 1 significant digit

I'd argue that the right-most 0 digit is telling us something (it's telling us about the resolution of the measurement), while the left-most one is not telling us anything. 0.00m and 0cm should have exactly the same amount of significant digits, like 0.01m and 1cm do.

Or also: what is that makes the left-most 0 in 0.00 significant, but does not make it significant in 0.01?

@github-actions
Copy link

github-actions bot commented Sep 23, 2025

PR Preview Action v1.6.2
Preview removed because the pull request was closed.
2025-09-24 19:38 UTC

@nicolo-ribaudo
Copy link
Member Author

Ok so, recap of where this PR is now.

It implements the significant digits semantics as I described in #54 (comment).
This also matches @gibson042's table in #66 (comment) with the exception of new Amount("0e-2"). It considers 0e-2 to be the same as 0.00, exactly as for any other digit <x>e-2 is the same as 0.0<x>.

It does not implement what I suggested in #66 (comment), which however I would be happy to migrate to (both approaches are consistent, it comes to a matter of personal preference I guess).

It possibly has a bug when it comes to new Amount("0.00").toString(), because the toString logic might not want to generate multiple leading zeroes. I still need to verify this, but the in-progress changes in #69 make it a bit difficult.

@waldemarhorwat About the invalid assumptions around negative fractionDigits: RoundAmountValueToFractionDigits indeed had a wrong signature, but the implementation should work also with a negative number. I'm still looking for the other cases.

@waldemarhorwat
Copy link

@waldemarhorwat About the invalid assumptions around negative fractionDigits: RoundAmountValueToFractionDigits indeed had a wrong signature, but the implementation should work also with a negative number. I'm still looking for the other cases.

I just did a search and found cases such as GetAmountOptions which throws for negative fractionDigits.

@jessealama jessealama merged commit 55c223d into tc39:main Sep 24, 2025
2 checks passed
@gibson042
Copy link
Member

@gibson042 Those rules do not work with numbers whose value is 0. We need to pick which zero is the one that matters. In 0.00:

  • if the one that matters is the left-most one, then it's like 1.00 and thus it has 3 significant digits
  • if the one that matters is the right-most one, then it's like 0.01 and thus it has 1 significant digit

I'd argue that the right-most 0 digit is telling us something (it's telling us about the resolution of the measurement), while the left-most one is not telling us anything. 0.00m and 0cm should have exactly the same amount of significant digits, like 0.01m and 1cm do.

Or also: what is that makes the left-most 0 in 0.00 significant, but does not make it significant in 0.01?

OK, I'll accept that argument.

It implements the significant digits semantics as I described in #54 (comment). This also matches @gibson042's table in #66 (comment) with the exception of new Amount("0e-2"). It considers 0e-2 to be the same as 0.00, exactly as for any other digit <x>e-2 is the same as 0.0<x>.

In what sense is that an exception? I haven't reviewed the changes that merged here, but did include above that new Amount("0e-2") should be treated as 0.00 and be recognized as 2 fraction digits [after the .] and 1 significant digit [the final 0]. Is that not the case?

@gibson042
Copy link
Member

@gibson042 Those rules do not work with numbers whose value is 0. We need to pick which zero is the one that matters. In 0.00:

  • if the one that matters is the left-most one, then it's like 1.00 and thus it has 3 significant digits
  • if the one that matters is the right-most one, then it's like 0.01 and thus it has 1 significant digit

I'd argue that the right-most 0 digit is telling us something (it's telling us about the resolution of the measurement), while the left-most one is not telling us anything. 0.00m and 0cm should have exactly the same amount of significant digits, like 0.01m and 1cm do.
Or also: what is that makes the left-most 0 in 0.00 significant, but does not make it significant in 0.01?

OK, I'll accept that argument.

But also, note that 0.00e${exp} clearly has three significant digits, as does 00.00e${exp} and [I would argue] .00e${exp}. But that's just the nature of exponential notation is not specific to zero values other than still ignoring leading non-units-place zeros and implying a units place as necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants