fix(xpath): format a large integer-valued number with no decimal point (#268)

XPath 1.0 4.2: an integer-valued number converts to a string with no decimal
point. The formatter only took the integer branch for a magnitude below 1e15,
so a value like 1234567890123456 (exact in a double, which holds integers
exactly to 2^53) printed as 1234567890123456.0. Raise the bound to 2^63, the
range where the Int64 conversion is safe; larger magnitudes still expand
through the exponent path.

Closes Apache Xalan conformance case string132.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 04:31:19 +02:00 committed by GitHub
parent 9070ccc3f1
commit 95fde5b177
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 16 additions and 2 deletions

View File

@ -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`.

View File

@ -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))

View File

@ -36,5 +36,4 @@ sort/sort27
sort/sort32
sort/sort33
sort/sort34
string/string132
whitespace/whitespace20

View File

@ -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")