PureXML/Sources/XPath/EvaluatorOperators.swift
Mihaela Mihaljevic d83e5a88ce feat(xpath): expression engine and the four-type model
Compile and evaluate full XPath 1.0 expressions, not just location paths.
Add the four-type Value (node-set, boolean, number, string) with the spec's
coercion rules and canonical number formatting, a recursive-descent parser
for the operator grammar (or/and, equality, relational, additive,
multiplicative, unary minus, union) whose grammatical position disambiguates
* and the operator keywords, function calls, variable references, and filter
expressions. Predicates are now arbitrary expressions (a numeric predicate
is a position test). Comparison follows the node-set quantification rules.
Query gains value/number/string/boolean accessors and a $variable binding
map; the core functions last, position, count, not, true, false, boolean,
number, and string ship with it.

Closes #42
2026-06-08 05:33:13 +02:00

114 lines
4.0 KiB
Swift

extension PureXML.XPath.Evaluator {
typealias BinaryOperator = PureXML.XPath.BinaryOperator
static func evalBinary(
_ oper: BinaryOperator,
_ left: Expression,
_ right: Expression,
_ context: EvaluationContext,
) throws -> Value {
switch oper {
case .logicalOr:
try .boolean(eval(left, context).boolean || eval(right, context).boolean)
case .logicalAnd:
try .boolean(eval(left, context).boolean && eval(right, context).boolean)
case .add, .subtract, .multiply, .divide, .modulo:
try .number(arithmetic(oper, eval(left, context).number, eval(right, context).number))
case .equal, .notEqual:
try .boolean(equality(eval(left, context), eval(right, context), negate: oper == .notEqual))
case .lessThan, .lessOrEqual, .greaterThan, .greaterOrEqual:
try .boolean(relational(eval(left, context), eval(right, context), oper))
}
}
private static func arithmetic(_ oper: BinaryOperator, _ lhs: Double, _ rhs: Double) -> Double {
switch oper {
case .add: lhs + rhs
case .subtract: lhs - rhs
case .multiply: lhs * rhs
case .divide: lhs / rhs
case .modulo: lhs.truncatingRemainder(dividingBy: rhs)
default: .nan
}
}
// MARK: Equality
private static func equality(_ lhs: Value, _ rhs: Value, negate: Bool) -> Bool {
switch (lhs, rhs) {
case let (.nodeSet(left), .nodeSet(right)):
nodeSetEquality(left, right, negate: negate)
case let (.nodeSet(nodes), scalar):
nodeSetScalarEquality(nodes, scalar, negate: negate)
case let (scalar, .nodeSet(nodes)):
nodeSetScalarEquality(nodes, scalar, negate: negate)
default:
scalarEquality(lhs, rhs, negate: negate)
}
}
private static func nodeSetEquality(_ left: [Node], _ right: [Node], negate: Bool) -> Bool {
let rightStrings = right.map(\.stringValue)
for value in left.map(\.stringValue) {
for other in rightStrings where (value == other) != negate {
return true
}
}
return false
}
private static func nodeSetScalarEquality(_ nodes: [Node], _ scalar: Value, negate: Bool) -> Bool {
switch scalar {
case let .boolean(flag):
(!nodes.isEmpty == flag) != negate
case let .number(number):
nodes.contains { (PureXML.XPath.Value.parseNumber($0.stringValue) == number) != negate }
case let .string(text):
nodes.contains { ($0.stringValue == text) != negate }
case .nodeSet:
false
}
}
private static func scalarEquality(_ lhs: Value, _ rhs: Value, negate: Bool) -> Bool {
let equal: Bool = if case .boolean = lhs { lhs.boolean == rhs.boolean } else if case .boolean = rhs {
lhs.boolean == rhs.boolean
} else if case .number = lhs { lhs.number == rhs.number } else if case .number = rhs {
lhs.number == rhs.number
} else {
lhs.string == rhs.string
}
return equal != negate
}
// MARK: Relational
private static func relational(_ lhs: Value, _ rhs: Value, _ oper: BinaryOperator) -> Bool {
let lefts = numbers(of: lhs)
let rights = numbers(of: rhs)
for left in lefts {
for right in rights where numericCompare(left, right, oper) {
return true
}
}
return false
}
private static func numbers(of value: Value) -> [Double] {
if case let .nodeSet(nodes) = value {
return nodes.map { PureXML.XPath.Value.parseNumber($0.stringValue) }
}
return [value.number]
}
private static func numericCompare(_ lhs: Double, _ rhs: Double, _ oper: BinaryOperator) -> Bool {
switch oper {
case .lessThan: lhs < rhs
case .lessOrEqual: lhs <= rhs
case .greaterThan: lhs > rhs
case .greaterOrEqual: lhs >= rhs
default: false
}
}
}