Skip to content

June spec review #155

Open
Open
@waldemarhorwat

Description

I took another look at the Decimal spec for the June meeting. The spec forms a good foundation, but I found some pervasive issues that make it self-contradictory or produce incorrect results and should be addressed before seeking stage 2.

My review follows. Some of the items are nontrivial and will require some discussion and agreement on how to fix. I'm offering to provide whatever help is needed to understand and fix these.

Confusion

  • Throughout the spec there are errors where a subroutine is called with different types of arguments or different data than what it expects. Document what the arguments should be and check all call sites to make sure that they match. One example is EnsureDecimal128Value, where part of the spec assumes that the first argument is the full mathematical value (which I believe to be the correct interpretation), while another part of the spec assumes that it's a mantissa. In some places multiple bugs overlap: The Decimal128 constructor confuses parse nodes, which causes Decimal128("3.14") to return 33.14𝔻; if the parse node confusion is fixed, then Decimal128("3.14") will return 314𝔻 due to EnsureDecimal128Value argument type confusion.
  • Handling of zeroes is incorrect in nearly all arithmetic operations due to the above type confusion. For example, Decimal128.prototype.add tries to do real number arithmetic on +0𝔻 and -0𝔻, which are not real numbers. IsIntegralDecimal128 returns false on +0𝔻 and -0𝔻.
  • All operations that produce new zeroes via arithmetic either throw or encounter the above type confusion.
  • Decimal128ToDecimalString confuses a value with its length and contains buffer overruns.

Rounding

  • None of the rounding modes is implemented.
  • The spec throws whenever an inexact operation happens. Dividing 1𝔻 by 3𝔻 will throw, as will adding 1050𝔻 to 1𝔻. This is a major change to the spec that I hadn't been expecting. Is this intentional?

Quantum Handling

  • Arithmetic on the quantum is incorrect in many places. If we're going to distinguish cohort members, we should have arithmetic operations set the quantum in the same way as the IEEE spec does it.
  • The spec neglects to set the quantum on zero values in methods such as Decimal128.prototype.round.
  • Per the IEEE spec, the quantum should not affect mathematical results. Interrogating the quantum in methods such as IsIntegralDecimal128 is incorrect.
  • The quantum appears in other places where it shouldn't, I'm guessing due to the confusion above. toPrecision with a user-provided precision shouldn't interrogate the quantum. It's not the exponent.

Canonicalization

  • The spec confuses string canonicalization with Decimal128 value canonicalization. Those are quite different concepts:
  • What we want is for toString to produce strings independent of the input quantum unless one explicitly sets canonicalize to false: All Decimal values with the same v should produce the same string.
  • The spec attempts define a CanonicalizeDecimal128 helper method that tries to pick a specific "canonical" Decimal128 value to represent a cohort, but that doesn't work:
    • The choices are to pick either the one with the highest possible q, or the one with the lowest possible q. That's what's needed in some arithmetic contexts.
    • Neither choice works correctly for both toString and toExponential.
  • To fix this, restructure the spec to delete CanonicalizeDecimal128 and anything like it and just make the conversions not depend on the quantum unless the user requests it.

Comparisons

  • The compare method should compare mathematical values and return one of -1, 0, 1, or NaN. The current implementation also compares the quantum, which turns it from a generally useful method to an extremely obscure one that should be omitted in the first version.
  • A method for the < comparison is provided, but >, ≤, ≥ are missing, and the latter two are nontrivial for users to implement correctly. Naively turning a ≤ b into !(b < a) is incorrect due to NaN's.

Conversions

  • Invoking toString on 101000𝔻 produces 1 followed by 1000 zeroes. This is fine if printing a BigInt, but is undesirable for floating-point numbers because it provides a false sense of precision. Users will expect that they can add 1 to it, and, being a Decimal, get 100…001, which obviously won't work. Instead, toString should follow existing Number precedent and switch to scientific notation to not produce significantly more digits than is possible to represent in a Decimal128 value. If someone really wants fixed-point notation, they can use a different method such as toFixed.
  • One would expect calling toString with canonicalize set to false to not canonicalize the output, but that's not what it does. It will sometimes canonicalize and sometimes not, depending on what Decimal128 value it sees. So canonicalize set to false actually means "maybe". It should mean "don't canonicalize".
  • None of the conversions respect the quantum of zero values. But they should: if canonicalize is set to false, then we do want to distinguish 0 from 0.0000 and 0e75.
  • The quantum of large Numbers converted to Decimal128 is inconsistent. Converting 1e20 yields a quantum of 0, but converting 1e21 yields a quantum of 21. As a fix, I recommend doing the logical equivalent of always converting Numbers to Decimal128 via scientific notation.

ECMAScript Mechanics

  • ECMAScript object identity is inconsistent. Some methods such as Decimal128.prototype.round sometimes reuse input values and sometimes produce fresh values equal to the input values. We should be consistent here.
  • Number(x) now calls toString on every x, of any type. This is an undesirable change in existing behavior and not necessary for correct Decimal128 semantics.
  • The spec uses the wrong grammar for converting strings to Decimal128. As a result, Decimal128("07") throws.

Missing Operations

Structure

  • The introduction section uses concepts such as "zero" and "finite" before defining them.
  • For readability I'd prefer to use spec methods such as cohort and quantum on Decimal128 values rather than [0] and [1].

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