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.
One-line repro
1.1,2.2and3.3arexs:decimalliterals, so this comparison must be exact. Saxon, BaseX and eXist-db all returntrue.What happens
fontoxpath represents
xs:decimalvalues as JSnumberand performs all numeric binary operations as float64 ops (theM(2,2,op)entries in the binary-operator dispatch table apply raw+/-/*//to every numeric type, including decimal and integer). So:What the spec requires
xs:decimalis 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.op:numeric-addetc.): when both operands arexs:decimal, the result isxs:decimalcomputed with decimal semantics — not the nearest float64. Float behaviour (0.1 + 0.2e0 → 0.30000000000000004) is only correct when an operand isxs: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, because336300.95 + 49243.65comes out as385544.60000000003and 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+ − × divexactly, 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 againstsrc/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.