Skip to content

xs:decimal arithmetic uses float64 — violates XPath decimal semantics (1.1 + 2.2 = 3.3 → false) #686

Description

@NeosiaNexus

One-line repro

import { evaluateXPath } from 'fontoxpath'; // 3.34.0
evaluateXPath('1.1 + 2.2 = 3.3'); // → false (expected: true)

1.1, 2.2 and 3.3 are xs:decimal literals, so this comparison must be exact. Saxon, BaseX and eXist-db all return true.

What happens

fontoxpath represents xs:decimal values as JS number and performs all numeric binary operations as float64 ops (the M(2,2,op) entries in the binary-operator dispatch table apply raw +/-/*// to every numeric type, including decimal and integer). So:

evaluateXPath('1.1 + 2.2');              // → 3.3000000000000003
evaluateXPath('336300.95 + 49243.65');   // → 385544.60000000003

What the spec requires

  • XDM: xs:decimal is an arbitrary-precision decimal type; XML Schema requires minimally conforming processors to support at least 18 decimal digits (totalDigits), and results of decimal arithmetic must be exact within supported precision.
  • F&O 3.1 §4.2 (op:numeric-add etc.): when both operands are xs:decimal, the result is xs:decimal computed with decimal semantics — not the nearest float64. Float behaviour (0.1 + 0.2e0 → 0.30000000000000004) is only correct when an operand is xs:double/xs:float (or untyped, which casts to double).

Real-world impact

We hit this running the official EN 16931 e-invoicing schematron (EU Commission validation artefacts) on fontoxpath: rule BR-O-08 false-positives on the official CEF example invoice XRechnung-O.xml, because 336300.95 + 49243.65 comes out as 385544.60000000003 and the = comparison against the invoice total fails by 5.8e-11. A compliance validator built on fontoxpath flags a valid invoice as broken, where Saxon-based validators accept it.

Fix direction

Short term (bounded): in the numeric branch of the binary-operation dispatch, when the result type resolves to xs:decimal/xs:integer (i.e. both operands are decimal subtypes — untyped/double/float operands have already been routed to float semantics at that point), perform + − × div exactly, e.g. BigInt over the scaled decimal representation of the operands. We run exactly this as a local patch on the dist bundle: it fixes the repro above and the EN 16931 false positive, keeps all double/float semantics untouched, and shows no measurable performance regression on a 34-invoice validation corpus (~100–950 asserts each). Happy to turn it into a PR against src/expressions/operators/arithmetic/ if you're open to it.

Long term: an exact decimal value representation, since aggregates (fn:sum) still accumulate in float64 even with exact operators.

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