An attribute node's document-order key bands by the attribute's index among its
owner's attributes, found with owner.attributes.firstIndex { $0.name == name }.
That linear scan ran for every attribute node, so sorting an element's K
attributes into document order was O(K^2): an 8000-attribute element took ~0.18s
and grew quadratically.
Cache a per-owner attribute-name-to-index map in SiblingIndexCache, built once
on the second lookup (a single lookup still scans), exactly as the sibling-index
cache already does for child positions. The index is the same first-occurrence
position; only its cost changes.
The 8000-attribute sort drops to ~0.01s (~16x) and is now linear. The full
suite, the W3C XPath and Apache Xalan corpora, the C14N suite (which orders
attributes byte-exactly), and the WASM build stay green; AttributeDocumentOrderTests
pins declaration order through the cached sort.
189 lines
8.2 KiB
Swift
189 lines
8.2 KiB
Swift
public extension PureXML.XPath {
|
|
/// An XPath node. Most nodes wrap a parent-aware ``PureXML/Model/TreeNode``
|
|
/// (element, text, CDATA, comment, processing instruction, or the document
|
|
/// root). Attributes and namespaces are not tree nodes, so XPath surfaces them
|
|
/// as nodes owned by their element, as the data model requires.
|
|
///
|
|
/// Equality and hashing are by identity: two wrappers of the same tree node,
|
|
/// attribute, or namespace compare equal, which is what node-set de-duplication
|
|
/// needs. ``documentOrder`` gives the total order XPath results are sorted into.
|
|
enum Node: Hashable {
|
|
case tree(PureXML.Model.TreeNode)
|
|
case attribute(owner: PureXML.Model.TreeNode, PureXML.Model.Attribute)
|
|
case namespace(owner: PureXML.Model.TreeNode, prefix: String, uri: String)
|
|
|
|
public static func == (lhs: Node, rhs: Node) -> Bool {
|
|
switch (lhs, rhs) {
|
|
case let (.tree(left), .tree(right)):
|
|
left === right
|
|
case let (.attribute(leftOwner, left), .attribute(rightOwner, right)):
|
|
leftOwner === rightOwner && left.name == right.name
|
|
case let (.namespace(leftOwner, leftPrefix, _), .namespace(rightOwner, rightPrefix, _)):
|
|
leftOwner === rightOwner && leftPrefix == rightPrefix
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
switch self {
|
|
case let .tree(node):
|
|
hasher.combine(0)
|
|
hasher.combine(ObjectIdentifier(node))
|
|
case let .attribute(owner, attribute):
|
|
hasher.combine(1)
|
|
hasher.combine(ObjectIdentifier(owner))
|
|
hasher.combine(attribute.name)
|
|
case let .namespace(owner, prefix, _):
|
|
hasher.combine(2)
|
|
hasher.combine(ObjectIdentifier(owner))
|
|
hasher.combine(prefix)
|
|
}
|
|
}
|
|
|
|
/// The wrapped tree node, when this is one (nil for attribute/namespace).
|
|
var treeNode: PureXML.Model.TreeNode? {
|
|
guard case let .tree(node) = self else { return nil }
|
|
return node
|
|
}
|
|
|
|
/// The owning element of an attribute or namespace node, or the tree
|
|
/// node's own parent for a tree node.
|
|
var parent: Node? {
|
|
switch self {
|
|
case let .tree(node):
|
|
node.parent.map(Node.tree)
|
|
case let .attribute(owner, _), let .namespace(owner, _, _):
|
|
.tree(owner)
|
|
}
|
|
}
|
|
|
|
/// The XPath string-value: an attribute's value, a namespace URI, or the
|
|
/// concatenated character data of a tree node's subtree.
|
|
var stringValue: String {
|
|
switch self {
|
|
case let .tree(node):
|
|
node.stringValue
|
|
case let .attribute(_, attribute):
|
|
attribute.value
|
|
case let .namespace(_, _, uri):
|
|
uri
|
|
}
|
|
}
|
|
|
|
/// The node's expanded name, or nil for nodes without a name (text, CDATA,
|
|
/// comment, document). For a namespace node the local part is the prefix.
|
|
var qualifiedName: PureXML.Model.QualifiedName? {
|
|
switch self {
|
|
case let .tree(node):
|
|
switch node.kind {
|
|
case .element, .processingInstruction:
|
|
node.name
|
|
default:
|
|
nil
|
|
}
|
|
case let .attribute(_, attribute):
|
|
attribute.name
|
|
case let .namespace(_, prefix, _):
|
|
PureXML.Model.QualifiedName(prefix)
|
|
}
|
|
}
|
|
|
|
/// The path of child indices from the document root, with negative bands
|
|
/// placing namespace nodes before attribute nodes before child nodes, so a
|
|
/// lexicographic compare yields XPath document order.
|
|
var documentOrder: [Int] {
|
|
switch self {
|
|
case let .tree(node):
|
|
return Self.treePath(node)
|
|
case let .attribute(owner, attribute):
|
|
let index = owner.attributes.firstIndex { $0.name == attribute.name } ?? 0
|
|
return Self.treePath(owner) + [Self.attributeBand, index]
|
|
case let .namespace(owner, prefix, _):
|
|
return Self.treePath(owner) + [Self.namespaceBand, prefix.hashValue]
|
|
}
|
|
}
|
|
|
|
private static let namespaceBand = -2
|
|
private static let attributeBand = -1
|
|
|
|
private static func treePath(_ node: PureXML.Model.TreeNode) -> [Int] {
|
|
treePath(node, cache: nil)
|
|
}
|
|
|
|
/// The root path, with an optional per-operation sibling-index cache:
|
|
/// without one, finding a node's index among its siblings is linear,
|
|
/// which turns a sort over a flat fan-out (one parent, n children)
|
|
/// quadratic. The cache enumerates each distinct parent once, making
|
|
/// total key computation linear in the tree.
|
|
static func treePath(_ node: PureXML.Model.TreeNode, cache: SiblingIndexCache?) -> [Int] {
|
|
var path: [Int] = []
|
|
var current = node
|
|
while let parent = current.parent {
|
|
path.append(cache?.index(of: current, in: parent) ?? parent.children.firstIndex { $0 === current } ?? 0)
|
|
current = parent
|
|
}
|
|
return path.reversed()
|
|
}
|
|
|
|
/// `documentOrder` with the sibling-index cache applied.
|
|
func documentOrder(cache: SiblingIndexCache) -> [Int] {
|
|
switch self {
|
|
case let .tree(node):
|
|
return Self.treePath(node, cache: cache)
|
|
case let .attribute(owner, attribute):
|
|
let index = cache.attributeIndex(of: attribute.name, in: owner)
|
|
return Self.treePath(owner, cache: cache) + [Self.attributeBand, index]
|
|
case let .namespace(owner, prefix, _):
|
|
return Self.treePath(owner, cache: cache) + [Self.namespaceBand, prefix.hashValue]
|
|
}
|
|
}
|
|
|
|
/// Whether `lhs` precedes `rhs` in document order. Each call recomputes
|
|
/// both order keys; for sorts and minimums over node-sets, prefer
|
|
/// ``Swift/Sequence/sortedByDocumentOrder()`` and
|
|
/// ``Swift/Sequence/firstInDocumentOrder()``, which compute each key once.
|
|
static func precedes(_ lhs: Node, _ rhs: Node) -> Bool {
|
|
ordered(lhs.documentOrder, before: rhs.documentOrder)
|
|
}
|
|
|
|
/// Lexicographic compare of two document-order keys.
|
|
static func ordered(_ left: [Int], before right: [Int]) -> Bool {
|
|
for (one, two) in zip(left, right) where one != two {
|
|
return one < two
|
|
}
|
|
return left.count < right.count
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Sequence<PureXML.XPath.Node> {
|
|
/// The nodes sorted into document order, computing each node's order key once
|
|
/// (decorate-sort-undecorate) rather than recomputing the root path on every
|
|
/// comparison, which turns an O(n log n x depth) sort into O(n log n).
|
|
func sortedByDocumentOrder() -> [PureXML.XPath.Node] {
|
|
// Zero- and one-node sets are already sorted: the common predicate
|
|
// case must not touch order keys at all.
|
|
let nodes = Array(self)
|
|
if nodes.count <= 1 { return nodes }
|
|
let cache = PureXML.XPath.SiblingIndexCache()
|
|
return nodes.map { (node: $0, key: $0.documentOrder(cache: cache)) }
|
|
.sorted { PureXML.XPath.Node.ordered($0.key, before: $1.key) }
|
|
.map(\.node)
|
|
}
|
|
|
|
/// The first node in document order, computing each node's order key once.
|
|
func firstInDocumentOrder() -> PureXML.XPath.Node? {
|
|
let nodes = Array(self)
|
|
// Zero or one node needs no ordering: the single node is trivially first.
|
|
// This is the common case for string-value extraction (`string(@x)`,
|
|
// `value-of`), where computing a document-order key would needlessly scan
|
|
// the node's siblings, turning a flat fan-out quadratic.
|
|
guard nodes.count > 1 else { return nodes.first }
|
|
let cache = PureXML.XPath.SiblingIndexCache()
|
|
return nodes.map { (node: $0, key: $0.documentOrder(cache: cache)) }
|
|
.min { PureXML.XPath.Node.ordered($0.key, before: $1.key) }?
|
|
.node
|
|
}
|
|
}
|