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
114 lines
4.0 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|