Rule Cascade
ReferenceDecision records (ADRs)

ADR 0003: Doubles on the way in, decimal arithmetic inside, 15 significant digits on the way out

Status: accepted. Extended for 1.0.0-alpha.2: how numbers enter, and that every number leaving is rounded.

Status: accepted. Extended for 1.0.0-alpha.2: how numbers enter, and that every number leaving is rounded.

Context

JavaScript numbers are binary doubles; Java has BigDecimal; Go and Python keep the digits of a JSON number if asked to; JSON has no number precision rule. 0.1 + 0.2 must equal 0.3 in a rule about money, in every runtime, and a number in a ruleset or a request must be the same number to every runtime that reads it.

Decision

  1. In. A JSON number in a ruleset, a bundle or a request denotes the IEEE 754 double nearest to what was written, and the engine computes with the shortest decimal that identifies that double. A number too large for a double, NaN and the infinities are refused.
  2. Inside. Arithmetic in an expression uses decimal128 semantics: 34 significant digits, round half even. Nothing is rounded between operators.
  3. Out. Every number leaving an expression, whether it was computed or merely passed through, is rounded half even to 15 significant digits. One whose magnitude is then larger than the largest double is an evaluation error.

Why

  • Decimal inside removes binary rounding surprises from comparisons such as amount * rate <= limit.
  • 15 significant digits is the most a decimal can carry through an IEEE 754 double and come back unchanged. Rounding to that on the way out means a value computed in Java, serialised to JSON and parsed in a browser is exactly the value the browser would have computed itself.
  • This was found the hard way: before the rule existed, the Java runtime returned 0.3333333333333333333333333333333333 where the others returned 0.3333333333333333.
  • The first version rounded only computed numbers and did not say what a number means on the way in. The cross-runtime checks showed the gap: a literal with more digits than a double holds was one number to a runtime that kept the digits and another to JavaScript, and a value that was only passed through could leave with different digits depending on the runtime. Reading every number as a double is what JSON.parse does and what most parsers do by default, so that is the meaning every runtime now gives a number, whatever type its JSON library delivers.

Consequences

  • Amounts up to 9,999,999,999,999.99 are exact. Larger values lose cents; such domains should use integer minor units.
  • Identifiers must be strings. 9007199254740993 is read as 9007199254740992 and leaves the engine as 9007199254740990.
  • Ruleset authors should not write numbers with more than 15 significant digits. rulecheck check and rule-cascade check report one as NUMBER_NOT_PORTABLE.
  • A custom operator receives numbers already rounded to 15 digits, and a number it returns is read as its shortest decimal; a non-finite result fails the rule closed.
  • Canonical JSON, and so every checksum, writes a number as the shortest decimal of its double.

On this page