Skip to content

Thoughts on Precision in Decimal #175

Open
@sffc

Description

@sffc

A topic that continues to be raised for discussion in the context of the Decimal proposal is the concept of precision, its interaction with arithmetic, and whether it should be part of the data model at all. I've been meaning to make a write-up for some time, and since we had a discussion on this subject in TG3 today, I thought this would be a good opportunity.

First, I want to start with the fundamental axiom of my position and all arguments that follow. The axiom is that the existence of trailing zeros makes certain values distinct from others. For example, "1" is distinct from "1.0", and "2.5" is distinct from "2.50". I will present evidence for this axiom:

  1. The two values are written and spoken differently.
  2. In language, the values influence the words around them. My favorite example: in English, one would say "I see one star in the sky" but "the average rating for this restaurant is one point zero stars".
  3. There is a large amount of precedent in software libraries that these values are distinct. Citation: Normalization in other implementations #89 (comment)

These are all evidence that "1", a natural number, and "1.0", a decimal approximation of something, are distinct values.

I consider this axiom to be a matter of fact, not a matter of opinion. To make an argument to the contrary would be to say that "1" and "1.0" are always fully interchangeable, CLDR is wrong to pluralize "star" in the string "1.0 stars", and software libraries are wrong to represent this in their data models.

Okay, now that I've established the fundamental axiom, I will make a case that core ECMAScript should have a way of representing these values as distinct.

  1. The distinct values influence behavior across multiple different localization operations. The formatting of numbers and the selection of plural rules, for example, both have different behavior depending on whether the number has trailing zeros. The upcoming Intl.MessageFormat may also feed the same value into multiple operations.
  2. That rounding, fractional digits, and trailing zeros impact plural rule selection is among the top preventable bugs I see in my experience in the i18n space, and the Decimal proposal presents an opportunity to make these bugs harder to write. I present examples and evidence in my 2020 post here: Merge PluralRules into NumberFormat (formatSelect) ecma402#397 (comment). Also see my example down below.
  3. There is extensive precedent of decimal libraries in other languages representing precision of the decimals (citation above, Normalization in other implementations #89 (comment)). Failing to represent precision in the Decimal data model would break from this long precedent.
  4. Given the extensive precedent, failure to represent precision in the data model makes us unable to round-trip values coming from other decimal implementations, harming our ability to interoperate with them.
  5. IEEE Decimal128 itself contains the concept of precision. By omitting the precision from the data model, ECMAScript is not following its stated reference specification, and it is in effect inventing its own representation of numbers. In the same way that Temporal should not invent concepts related to date representation (such as the IXDTF string syntax), Decimal should not invent concepts related to numeric data models.
  6. Representing the precision of a number has applications in scientific computing and accounting. Given that these use cases are amongst those that motivate Decimal in the first place, we should consider variable precision to be fundamental to the value proposition of the proposal.

Points 1 and 2 (that the problem exists and that the problem causes real bugs in the wild) are the ones of primary concern to me as an i18n advocate in TC39. Points 3-6 are additional ones I offer.

I will now address three counter-arguments that were raised in the TG3 call today.

@ljharb pointed out that a person often first comes across the concept of precision in numbers in a physics course and that it is often a hard concept to grasp. This is a true statement, and it could perhaps be used as evidence that it is confusing for decimal arithmetic to propagate precision. However, it is not an argument that the concept doesn't exist or whether the concept has applications relevant to the Decimal proposal.

@erights pointed out that the numbers π and ⅓ are distinct numerical concepts also not representable by a Decimal. This is again a true statement. However, I do not see how it leads logically to an argument that 1.0 and 2.50 should be excluded from Decimal. That 1.0 and 2.50 have applications to the Decimal proposal is not changed by the existence of π and ⅓.

Someone else, I think @nicolo-ribaudo, pointed out that representing the precision of decimals is a concern about how to display the mathematical value, i.e., a formatting option, not a concern for the data model. This is a valid position to hold and one I've often seen. My counter-argument is that internationalization's job is to take distinct values and display them for human consumption. Intl's job is to decide what numbering system to use, what symbols to use for decimal and grouping separators, whether to display grouping separators, and where to render plus and minus signs, for example. It is not Intl's job to decide whether to display trailing zeros, since making that decision changes the input from one distinct value to a different distinct value. Relatedly, it is also not generally Intl's job to decide the magnitude to which it should round numbers.

I will close with one more thought. Decimal128 representing trailing zeros does not by itself prevent the i18n bugs noted above. However, it sets us on a path where the cleanest, simplest code is the code that produces the correct i18n behavior. For example, in 2024, code that correctly calculates and renders a restaurant rating would look like this:

function formatRestaurantRatingInEnglish(ratings) {
  let avg = ratings.reduce((sum, x) => sum + x, 0) / ratings.length;
  let formatOptions = { minimumFractionDigits: 1, maximumFractionDigits: 1 };
  let pluralRules = new Intl.PluralRules("en", formatOptions);
  let formattedNumber = avg.toLocaleString("en", formatOptions);
  if (pluralRules.select(avg) === "one") {
    return `${formattedNumber} star`;
  } else {
    return `${formattedNumber} stars`;
  }
}

Note that an identical formatOptions must be passed as an argument to both new Intl.PluralRules and Number.prototype.toLocaleString (or equivalently new Intl.NumberFormat); if it is not, you have a bug. However, with a Decimal that represents trailing zeros, the code can be written like this:

function formatRestaurantRatingInEnglish(ratings) {
  let avg = ratings.reduce((sum, x) => sum.add(x), Decimal128.zero())
    .divide(ratings.length)
    .roundToMagnitude(-1);
  let pluralRules = new Intl.PluralRules("en");
  let formattedNumber = avg.toLocaleString("en");
  if (pluralRules.select(avg) === "one") {
    return `${formattedNumber} star`;
  } else {
    return `${formattedNumber} stars`;
  }
}

My unwavering principle in API design is that the easiest code to write should be the code that produces the correct results. We cannot prevent people from doing the wrong thing in a Turing-complete language, but it is our core responsibility as library designers to nudge developers in the right direction.

Also CC: @jessealama @ctcpip @littledan

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions