Node-set ordering previously recomputed both nodes' root paths on every precedes comparison (treePath is O(depth x siblings), so sorts were O(n log n x depth)). Add decorate-sort-undecorate helpers, sortedByDocumentOrder() and firstInDocumentOrder(), that compute each node's order key once, and switch the evaluator's order-and-dedup, the EXSLT set functions, string-value coercion, and NodeFunctions' target lookup to them. set:leading/trailing also decorate once, reusing the keys for the pivot filter and the final sort. precedes stays for pairwise use, sharing the key comparator (Node.ordered). Behavior unchanged, all suites green. Closes #113
156 lines
6.4 KiB
Swift
156 lines
6.4 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] {
|
|
var path: [Int] = []
|
|
var current = node
|
|
while let parent = current.parent {
|
|
let index = parent.children.firstIndex { $0 === current } ?? 0
|
|
path.append(index)
|
|
current = parent
|
|
}
|
|
return path.reversed()
|
|
}
|
|
|
|
/// 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] {
|
|
map { (node: $0, key: $0.documentOrder) }
|
|
.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? {
|
|
map { (node: $0, key: $0.documentOrder) }
|
|
.min { PureXML.XPath.Node.ordered($0.key, before: $1.key) }?
|
|
.node
|
|
}
|
|
}
|