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
128 lines
6.0 KiB
Swift
128 lines
6.0 KiB
Swift
extension PureXML.XPath {
|
|
/// The EXSLT extension functions (http://exslt.org/...), dispatched by the
|
|
/// namespace a function's prefix resolves to: the `math`, `sets`, and the
|
|
/// `common` modules. Registered through ``FunctionTable`` so they are
|
|
/// available wherever a prefix is bound to an EXSLT namespace.
|
|
enum EXSLT {
|
|
static let common = "http://exslt.org/common"
|
|
static let math = "http://exslt.org/math"
|
|
static let sets = "http://exslt.org/sets"
|
|
|
|
/// The implementation for an EXSLT `local` name in namespace `uri`, or nil.
|
|
static func implementation(uri: String, local: String) -> FunctionImplementation? {
|
|
switch uri {
|
|
case math: mathFunction(local)
|
|
case sets: setsFunction(local)
|
|
case common: commonFunction(local)
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
// MARK: common
|
|
|
|
private static func commonFunction(_ local: String) -> FunctionImplementation? {
|
|
guard local == "object-type" else { return nil }
|
|
return { arguments, _ in
|
|
switch arguments.first {
|
|
case .nodeSet: .string("node-set")
|
|
case .boolean: .string("boolean")
|
|
case .number: .string("number")
|
|
case .string, .none: .string("string")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: math
|
|
|
|
private static func mathFunction(_ local: String) -> FunctionImplementation? {
|
|
switch local {
|
|
case "min": { arguments, _ in .number(extremum(arguments.first, isMin: true)) }
|
|
case "max": { arguments, _ in .number(extremum(arguments.first, isMin: false)) }
|
|
case "highest": { arguments, _ in .nodeSet(extremeNodes(arguments.first, isMin: false)) }
|
|
case "lowest": { arguments, _ in .nodeSet(extremeNodes(arguments.first, isMin: true)) }
|
|
case "abs": { arguments, _ in .number(abs(arguments.first?.number ?? .nan)) }
|
|
case "sqrt": { arguments, _ in .number((arguments.first?.number ?? .nan).squareRoot()) }
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
/// The minimum or maximum of a node-set's numeric values; NaN for an empty
|
|
/// set or any non-numeric node (the EXSLT rule).
|
|
private static func extremum(_ value: Value?, isMin: Bool) -> Double {
|
|
guard let nodes = value?.nodes, !nodes.isEmpty else { return .nan }
|
|
let numbers = nodes.map { PureXML.XPath.Value.parseNumber($0.stringValue) }
|
|
if numbers.contains(where: \.isNaN) { return .nan }
|
|
return isMin ? numbers.min() ?? .nan : numbers.max() ?? .nan
|
|
}
|
|
|
|
/// The nodes whose numeric value equals the set's maximum (or minimum),
|
|
/// in document order; empty when any value is NaN.
|
|
private static func extremeNodes(_ value: Value?, isMin: Bool) -> [Node] {
|
|
guard let nodes = value?.nodes, !nodes.isEmpty else { return [] }
|
|
let target = extremum(value, isMin: isMin)
|
|
guard !target.isNaN else { return [] }
|
|
return nodes
|
|
.filter { PureXML.XPath.Value.parseNumber($0.stringValue) == target }
|
|
.sortedByDocumentOrder()
|
|
}
|
|
|
|
// MARK: sets
|
|
|
|
private static func setsFunction(_ local: String) -> FunctionImplementation? {
|
|
switch local {
|
|
case "distinct": { arguments, _ in .nodeSet(distinct(arguments.first)) }
|
|
case "difference": { arguments, _ in .nodeSet(difference(arguments)) }
|
|
case "intersection": { arguments, _ in .nodeSet(intersection(arguments)) }
|
|
case "has-same-node": { arguments, _ in .boolean(hasSameNode(arguments)) }
|
|
case "leading": { arguments, _ in .nodeSet(relative(arguments, leading: true)) }
|
|
case "trailing": { arguments, _ in .nodeSet(relative(arguments, leading: false)) }
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
private static func distinct(_ value: Value?) -> [Node] {
|
|
guard let nodes = value?.nodes else { return [] }
|
|
var seen: Set<String> = []
|
|
var result: [Node] = []
|
|
for node in nodes.sortedByDocumentOrder() where seen.insert(node.stringValue).inserted {
|
|
result.append(node)
|
|
}
|
|
return result
|
|
}
|
|
|
|
private static func difference(_ arguments: [Value]) -> [Node] {
|
|
let first = arguments.first?.nodes ?? []
|
|
let second = Set(arguments.count > 1 ? arguments[1].nodes ?? [] : [])
|
|
return first.filter { !second.contains($0) }.sortedByDocumentOrder()
|
|
}
|
|
|
|
private static func intersection(_ arguments: [Value]) -> [Node] {
|
|
let first = arguments.first?.nodes ?? []
|
|
let second = Set(arguments.count > 1 ? arguments[1].nodes ?? [] : [])
|
|
return first.filter { second.contains($0) }.sortedByDocumentOrder()
|
|
}
|
|
|
|
private static func hasSameNode(_ arguments: [Value]) -> Bool {
|
|
let second = Set(arguments.count > 1 ? arguments[1].nodes ?? [] : [])
|
|
return (arguments.first?.nodes ?? []).contains { second.contains($0) }
|
|
}
|
|
|
|
/// `set:leading` / `set:trailing`: the nodes of the first set that precede
|
|
/// (or follow) the first node, in document order, of the second set.
|
|
private static func relative(_ arguments: [Value], leading: Bool) -> [Node] {
|
|
let first = arguments.first?.nodes ?? []
|
|
guard let pivot = (arguments.count > 1 ? arguments[1].nodes ?? [] : []).firstInDocumentOrder() else {
|
|
return []
|
|
}
|
|
// Decorate once: each node's order key is computed a single time and
|
|
// reused for both the pivot comparison and the final sort.
|
|
let pivotKey = pivot.documentOrder
|
|
return first
|
|
.map { (node: $0, key: $0.documentOrder) }
|
|
.filter { leading ? Node.ordered($0.key, before: pivotKey) : Node.ordered(pivotKey, before: $0.key) }
|
|
.sorted { Node.ordered($0.key, before: $1.key) }
|
|
.map(\.node)
|
|
}
|
|
}
|
|
}
|