Skip to content

Commit

Permalink
#25: Filter by property
Browse files Browse the repository at this point in the history
  • Loading branch information
trasch committed Sep 4, 2024
1 parent a75e9b5 commit d76833d
Show file tree
Hide file tree
Showing 4 changed files with 356 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Sources/MVTTools/Extensions/DoubleExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

extension Float {

var asDouble: Double { Double(self) }

}
9 changes: 9 additions & 0 deletions Sources/MVTTools/Extensions/IntExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

extension BinaryInteger {

var asInt: Int { Int(self) }

var asUInt: UInt { UInt(self) }

}
229 changes: 229 additions & 0 deletions Sources/MVTTools/QueryParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import Foundation

public struct QueryParser {

public enum Expression {
// Comparisons
public enum Comparison {
case equals
case notEquals
case greaterThan
case greaterThanOrEqual
case lessThan
case lessThanOrEqual
}

// Conditions
public enum Condition {
case and
case or
case not
}

case comparison(Comparison)
case condition(Condition)
case literal(Sendable)
case valueAt(Int)
case valueFor([String])
}

private let reader: Reader?
private var pipeline: [Expression]?

public init(string: String) {
self.reader = Reader(characters: Array(string.utf8))
self.parseQuery()
}

public init(pipeline: [Expression]) {
self.reader = nil
self.pipeline = pipeline
}

public func evaluate(on properties: [String: Sendable]) -> Bool {
guard let pipeline else { return false }

var stack: [Sendable?] = []

for expression in pipeline {
switch expression {
case let .comparison(condition):
switch condition {
case .equals, .notEquals:
guard stack.count >= 2,
let second = stack.removeFirst() as? AnyHashable,
let first = stack.removeFirst() as? AnyHashable
else { return false }

if condition == .equals {
stack.insert(first == second, at: 0)
}
else {
stack.insert(first != second, at: 0)
}

case .greaterThan, .greaterThanOrEqual, .lessThan, .lessThanOrEqual:
guard stack.count >= 2,
let second = stack.removeFirst(),
let first = stack.removeFirst()
else { return false }

stack.insert(compare(first: first, second: second, condition: condition), at: 0)
}

case let .condition(condition):
switch condition {
case .and, .or:
guard stack.count >= 2 else { return false }

let second = stack.removeFirst()
let first = stack.removeFirst()
let firstIsTrue = if let bool = first as? Bool { bool } else { first != nil }
let secondIsTrue = if let bool = second as? Bool { bool } else { second != nil }

if condition == .and {
stack.insert(firstIsTrue && secondIsTrue, at: 0)
}
else {
stack.insert(firstIsTrue || secondIsTrue, at: 0)
}

case .not:
guard stack.isNotEmpty else { return false }

let value = stack.removeFirst()
let valueIsTrue = if let bool = value as? Bool { bool } else { value != nil }

stack.insert(!valueIsTrue, at: 0)
}

case let .literal(value):
stack.insert(value, at: 0)

case let .valueAt(index):
guard stack.isNotEmpty,
let array = stack.removeFirst() as? [Sendable]
else { return false}

stack.insert(array.get(at: index), at: 0)

case let .valueFor(keys):
var current: Sendable? = properties

for key in keys {
if let object = current as? [String: Sendable] {
current = object[key]
}
else {
current = nil
break
}
}

stack.insert(current, at: 0)
}
}

// The stack should contain the result now
guard stack.count == 1,
let result = stack.first
else { return false }

if let bool = result as? Bool {
return bool
}

return result != nil
}

// This needs improvement - can this be done in a more generic way?
// Only the most common cases covered for now
private func compare(first: Sendable, second: Sendable, condition: QueryParser.Expression.Comparison) -> Bool {
if let left = (first as? Int) ?? (first as? Int8)?.asInt ?? (first as? Int16)?.asInt ?? (first as? Int32)?.asInt ?? (first as? Int64)?.asInt {
if let right = (second as? Int) ?? (second as? Int8)?.asInt ?? (second as? Int16)?.asInt ?? (second as? Int32)?.asInt ?? (second as? Int64)?.asInt {
return compare(left: left, right: right, condition: condition)
}
else if let right = (second as? UInt)?.asInt ?? (second as? UInt8)?.asInt ?? (second as? UInt16)?.asInt ?? (second as? UInt32)?.asInt ?? (second as? UInt64)?.asInt {
return compare(left: left, right: right, condition: condition)
}
else if let right = (second as? Double) ?? (second as? Float)?.asDouble {
return compare(left: Double(left), right: right, condition: condition)
}
}
else if let left = (first as? Double) ?? (first as? Float)?.asDouble {
if let right = (second as? Double) ?? (second as? Float)?.asDouble {
return compare(left: left, right: right, condition: condition)
}
else if let right = (second as? Int) ?? (second as? Int8)?.asInt ?? (second as? Int16)?.asInt ?? (second as? Int32)?.asInt ?? (second as? Int64)?.asInt {
return compare(left: left, right: Double(right), condition: condition)
}
}
if let left = (first as? UInt) ?? (first as? UInt8)?.asUInt ?? (first as? UInt16)?.asUInt ?? (first as? UInt32)?.asUInt ?? (first as? UInt64)?.asUInt {
if let right = (second as? UInt) ?? (second as? UInt8)?.asUInt ?? (second as? UInt16)?.asUInt ?? (second as? UInt32)?.asUInt ?? (second as? UInt64)?.asUInt {
return compare(left: left, right: right, condition: condition)
}
else if let right = (second as? Int)?.asUInt ?? (second as? Int8)?.asUInt ?? (second as? Int16)?.asUInt ?? (second as? Int32)?.asUInt ?? (second as? Int64)?.asUInt {
return compare(left: left, right: right, condition: condition)
}
else if let right = (second as? Double) ?? (second as? Float)?.asDouble {
return compare(left: Double(left), right: right, condition: condition)
}
}
else if let left = first as? String, let right = second as? String {
return compare(left: left, right: right, condition: condition)
}

return false
}

private func compare<T: Comparable>(left: T, right: T, condition: QueryParser.Expression.Comparison) -> Bool {
switch condition {
case .equals: left == right
case .notEquals: left != right
case .greaterThan: left > right
case .greaterThanOrEqual: left >= right
case .lessThan: left < right
case .lessThanOrEqual: left <= right
}
}

private mutating func parseQuery() {

}

// MARK: - Reader

struct Reader {

let characters: [UInt8]

private var index: Int = 0

init(characters: [UInt8]) {
self.characters = characters
}

mutating func readNextCharacter() -> UInt8? {
guard index < characters.endIndex else {
index = characters.endIndex
return nil
}

defer { index += 1 }

return characters[index]
}

func peek(withOffset offset: Int = 0) -> UInt8? {
guard index + offset < characters.endIndex else { return nil }

return characters[index + offset]
}

mutating func moveIndex(by offset: Int) {
index += offset
}

}

}
111 changes: 111 additions & 0 deletions Tests/MVTToolsTests/QueryParserTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import XCTest

@testable import MVTTools

final class QueryParserTests: XCTestCase {

private static let properties: [String: Sendable] = [
"foo": [
"bar": 1,
],
"some": [
"a",
"b",
],
"value": 1,
]

private func result(for pipeline: [QueryParser.Expression]) -> Bool {
QueryParser(pipeline: pipeline).evaluate(on: QueryParserTests.properties)
}

func testValues() throws {
XCTAssertTrue(result(for: [.valueFor(["foo"])]))
XCTAssertTrue(result(for: [.valueFor(["foo", "bar"])]))
XCTAssertFalse(result(for: [.valueFor(["foo", "x"])]))
XCTAssertFalse(result(for: [.valueFor(["foo.bar"])]))
XCTAssertFalse(result(for: [.valueFor(["foo"]), .valueAt(0)]))
XCTAssertTrue(result(for: [.valueFor(["some"]), .valueAt(0)]))
}

func testComparisons() throws {
XCTAssertFalse(result(for: [.valueFor(["value"]), .literal("bar"), .comparison(.equals)]))
XCTAssertTrue(result(for: [.valueFor(["value"]), .literal(1), .comparison(.equals)]))
XCTAssertTrue(result(for: [.valueFor(["value"]), .literal(1.0), .comparison(.equals)]))
XCTAssertFalse(result(for: [.valueFor(["value"]), .literal(1), .comparison(.notEquals)]))
XCTAssertFalse(result(for: [.valueFor(["value"]), .literal(1), .comparison(.greaterThan)]))
XCTAssertTrue(result(for: [.valueFor(["value"]), .literal(1), .comparison(.greaterThanOrEqual)]))
XCTAssertTrue(result(for: [.valueFor(["value"]), .literal(0.5), .comparison(.greaterThanOrEqual)]))
XCTAssertFalse(result(for: [.valueFor(["value"]), .literal(1), .comparison(.lessThan)]))
XCTAssertTrue(result(for: [.valueFor(["value"]), .literal(1), .comparison(.lessThanOrEqual)]))
XCTAssertTrue(result(for: [.valueFor(["value"]), .literal(1.5), .comparison(.lessThanOrEqual)]))
XCTAssertFalse(result(for: [.valueFor(["x"]), .literal(1), .comparison(.equals)]))
}

func testConditions() throws {
XCTAssertTrue(result(for: [
.valueFor(["foo", "bar"]),
.literal(1),
.comparison(.equals),
.valueFor(["value"]),
.literal(1),
.comparison(.equals),
.condition(.and),
]))
XCTAssertFalse(result(for: [
.valueFor(["foo"]),
.literal(1),
.comparison(.equals),
.valueFor(["bar"]),
.literal(2),
.comparison(.equals),
.condition(.or),
]))
XCTAssertTrue(result(for: [
.valueFor(["foo"]),
.literal(1),
.comparison(.equals),
.valueFor(["value"]),
.literal(1),
.comparison(.equals),
.condition(.or),
]))
XCTAssertFalse(result(for: [
.valueFor(["foo"]),
.condition(.not),
]))
XCTAssertTrue(result(for: [
.valueFor(["foo"]),
.valueFor(["bar"]),
.condition(.and),
.condition(.not),
]))
XCTAssertFalse(result(for: [
.valueFor(["foo"]),
.valueFor(["some"]),
.condition(.and),
.condition(.not),
]))
XCTAssertFalse(result(for: [
.valueFor(["foo"]),
.valueFor(["bar"]),
.condition(.or),
.condition(.not),
]))
XCTAssertTrue(result(for: [
.valueFor(["x"]),
.valueFor(["y"]),
.condition(.or),
.condition(.not),
]))
XCTAssertFalse(result(for: [
.valueFor(["foo", "bar"]),
.condition(.not),
]))
XCTAssertTrue(result(for: [
.valueFor(["foo", "x"]),
.condition(.not),
]))
}

}

0 comments on commit d76833d

Please sign in to comment.