-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
356 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import Foundation | ||
|
||
extension Float { | ||
|
||
var asDouble: Double { Double(self) } | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
])) | ||
} | ||
|
||
} |