diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d840e6..a3d3999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A large integer-valued number now converts to a string with no decimal point (XPath 1.0 4.2). The number-to-string formatter only treated a value below 1e15 as an integer, so an integer such as `1234567890123456` (still exact in a double, which represents integers exactly up to 2^53) printed as `1234567890123456.0`. The integer bound is now 2^63, the range in which the conversion is safe. Closes Apache Xalan conformance case `string132`. + - A lone `/` (the root node) is now parsed correctly when it is followed by something other than the end of the expression, such as a `)`, `]`, `|`, or an operator. The XPath grammar makes the relative part after `/` optional, but the compiler only treated `/` as the root path at the very end of the expression, so `count(/)`, `generate-id(/)`, and `/ = .` tried to parse a step from the following token and yielded an empty node-set. The compiler now recognizes a `/` as the root whenever no location step follows it. Closes Apache Xalan conformance cases `position106` and `mdocs15`. - A template match pattern written with an explicit `child::` or `attribute::` axis now gets the default priority of its node test (XSLT 1.0 5.5), not 0. `attribute::node()` is a `node()` node test, so its default priority is -0.5 (the same as `@*`), but it was treated as a name test and given priority 0, which made it wrongly outrank `@*`. The axis is now stripped before the node test is classified. Closes Apache Xalan conformance cases `conflictres29` and `conflictres30`. diff --git a/Sources/XPath/Value.swift b/Sources/XPath/Value.swift index e25ffc0..431a4c4 100644 --- a/Sources/XPath/Value.swift +++ b/Sources/XPath/Value.swift @@ -58,7 +58,11 @@ public extension PureXML.XPath { static func format(_ value: Double) -> String { if value.isNaN { return "NaN" } if value.isInfinite { return value < 0 ? "-Infinity" : "Infinity" } - if value == value.rounded(), abs(value) < 1e15 { + // An integer-valued double prints with no decimal point. The bound is + // 2^63 so the Int64 conversion cannot trap; exact integers run to 2^53 + // and the representable ones above it are still whole, and a magnitude + // beyond 2^63 falls to the exponent expansion below. + if value == value.rounded(), abs(value) < 0x1p63 { return String(Int64(value)) } return expandExponent(String(value)) diff --git a/Tests/Fixtures/xalan-baseline.txt b/Tests/Fixtures/xalan-baseline.txt index f0f6fe9..413e1ad 100644 --- a/Tests/Fixtures/xalan-baseline.txt +++ b/Tests/Fixtures/xalan-baseline.txt @@ -36,5 +36,4 @@ sort/sort27 sort/sort32 sort/sort33 sort/sort34 -string/string132 whitespace/whitespace20 diff --git a/Tests/XPathExpressionTests.swift b/Tests/XPathExpressionTests.swift index 1376eba..f476286 100644 --- a/Tests/XPathExpressionTests.swift +++ b/Tests/XPathExpressionTests.swift @@ -58,6 +58,15 @@ struct XPathExpressionTests { #expect(try string("0 div 0") == "NaN") } + @Test("a large integer-valued number prints with no decimal point") + func test_largeIntegerFormatting() throws { + // XPath 4.2: an integer-valued number has no decimal point, including a + // large integer still exact in a double (Apache Xalan string132). + #expect(try string("1234567890123456") == "1234567890123456") + #expect(try string("-1234567890123456") == "-1234567890123456") + #expect(try string("123456789012345 * 10") == "1234567890123450") + } + @Test("Booleans coerce to strings and numbers") func test_booleanCoercion() throws { #expect(try string("1 = 1") == "true")