Skip to content

Instantly share code, notes, and snippets.

@ahayman
Created July 25, 2015 00:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ahayman/909794a198a85461ad52 to your computer and use it in GitHub Desktop.
Save ahayman/909794a198a85461ad52 to your computer and use it in GitHub Desktop.
//
// Predicate.swift
//
// Created by Aaron Hayman on 6/29/15.
import Foundation
/**
##Predicate
A predicate is used as a filter that is applied to Datasets. In many ways, it can be thought of as a type of Query on a specific dataset. Predicate is designed to be syntatically pleasing as possible while providing type safety and an object-oriented approach to constructing queries. It allows for a precise syntax as opposed to typing in text, which can be error prone.
It is converted into a NSPredicate using the `nsPredicate()` function. This allows you to use Predicate where ever you might need a NSPredicate, so it's functionality can extend beyond the confines of the Persistence Layer (if you prefer this syntax over NSPredicate string interpolation).
A predicate is initialized either with a single comparison or an array of comparisons. If you pass in an array of Comparisons, you'll need to choose how those comparisons are compounded together (And/Or) by specifying the correct initializer:
- *Single Predicate*: `Predicate("Age", .Is, 29)`
- *Compound And*: `Predicate(.And, [("Age", .Is, 29), ("Name", .Contains, "John")])`. Person who's age is 29 *and* Name contains John.
- *Compound Or*: `Predicate(.Or, [("Age", .Is, 29), ("Age", .Is, 30)])`. Person who's age is 29 or 30
Predicates can be nested, but you have to initialize individual Predicates into an array to do so.
Predicate(.And, [Predicate("Name", .Contains, "John"),
Predicate(.Or, [("Age", .Is, 29),
("Age", .Is, 30)])])
The above would resolve to: "If persons Name contains john and that person is either age 29 or 30".
Predicates can be chained. This can be syntatically pleasing (IMO) but you should be aware of how chaining works. Essentially, chaining will append your Predicate(s) to the current set *if* the current set using the same Compounder (or no compounder) as the set you append. However, if you append object(s) with a different compounder, those objects will constitute a new set and will be compounded with the old set using the compounder you specified. Some examples, given for pre-established predicates: predicat0, predicate1, predicate2, predicate3:
Example 1:
let predicate = Predicate(.And, [predicate0, predicate1])
let newPredicate = predicate.and(.And, [predicate2, predicate3])
The `newPredicate` will now be "predicate0 AND predicate1 AND predicate2 AND predicate3"
Example 2:
let predicate = Predicate(.And: [predicate0, predicate1])
let newPredicate = predicate.or(.And, [predicate2, predicate3])
The `newPredicate` will now be "(predicate0 AND predicate1) OR (predicate3 AND predicate4)"
Example 3:
let predicate = Predicate(.Or, [predicate0, predicate1])
let newPredicate = predicate.and(.Or, [predicate2, predicate3])
The `newPredicate` will now be "(predicate0 OR predicate1) AND (predicate3 OR predicate4)"
This rule applies to single predicate chaining as well (think of it as adding an array of one item). While convenient, please note that this does not prevent you from chaining together weird or illogical predicates. It will help if you can consider the `add`, `or` functions as *appending* the predicates. For this reason, it is important that you are explicit in your groupings.
Example 1 (good):
let predicate = Predicate("Age", .Is, 29).or("Age", .Is, 30)
The `predicate` is "Age IS 29 OR Age IS 30"
Example 2 (bad):
let predicate = Predicate("Name", .Contains, "John").and("Age", .Is, 29).or("Age", .Is, 30)
The `predicate` is "(Name CONTAINS John AND Age IS 29) OR Age IS 30" - This is probably not what you wanted: We get a John that is 29, or anyone that is 30
Example 3 (good: explicit groupings):
let predicate = Predicate("Name", .Contains, "John").and(.Or, [("Age", .Is, 29), ("Age", .Is, 30)])
The `predicate` is "Name CONTAINS John AND (Age IS 29 OR Age IS 30)" - Better, we have a John that is either 29 or 30
Example 4 (great: better comparison operators)
let predicate = Predicate("Name", .Contains, "John").or("Age", .In, [29, 30])
let predicate = Predicate("Name", .Contains, "John").or("Age", .Between, [29, 30])
*/
public enum Comparison : String {
case Is = "=="
case GreaterThanEqual = ">="
case GreaterThan = ">"
case LessThanEqual = "<="
case LessThan = "<"
case Not = "!="
case In = "IN"
case Between = "BETWEEN"
case BeginsWith = "BEGINSWITH"
case EndWith = "ENDWITH"
case Contains = "CONTAINS"
case ContainsInsensitive = "CONTAINS[c]"
}
public enum Compounder {
case Or
case And
}
private struct CompoundPredicate {
let compounder: Compounder
let predicates: [Predicate]
}
private struct SinglePredicate {
let field: String
let comparison: Comparison
let value: AnyObject
}
public struct Predicate {
private let predicate: Either<SinglePredicate,CompoundPredicate>
/**
Initializes an array of predicates compounded with the provided compounder.
*/
public init(_ compounder: Compounder, _ predicates: [Predicate]) {
predicate = Either(CompoundPredicate(compounder: compounder, predicates: predicates))
}
/**
Initializes with a single predicate
*/
private init(_ predicate: SinglePredicate) {
self.predicate = Either(predicate)
}
public init(_ field: String, _ comparison: Comparison, _ value: AnyObject) {
self.init(SinglePredicate(field: field, comparison: comparison, value: value))
}
/**
This initalizes a predicate with an array of predicates compounded with the provided compounder.
*/
public init(_ compounder: Compounder, _ predicates: [(String, Comparison, AnyObject)]) {
self.init(compounder, predicates.map{ Predicate($0.0, $0.1, $0.2) })
}
/**
Convenience function: This will compound a single Predicate using .And
*/
public func and(field: String, _ comparison: Comparison, _ value: AnyObject) -> Predicate {
return and(.And, [(field, comparison, value)])
}
/**
Convenience function. This converts the tuples to Predicates and calls `and` with the predicates
*/
public func and(compounder: Compounder, _ predicates: [(String, Comparison, AnyObject)]) -> Predicate {
return and(compounder, predicates.map{ Predicate($0.0, $0.1, $0.2) })
}
/**
Convenience function. This will call `and` using the .And compounder with the Predicate
*/
public func and(predicate: Predicate) -> Predicate {
return and(.And, [predicate])
}
/**
This will compound an array of compounded predicates (using the povide compounder) to this predicate using .And
*/
public func and(compounder: Compounder, _ predicates: [Predicate]) -> Predicate {
return predicate.map(mapLeft: { singlePredicate in
switch compounder {
case .And: return Predicate(.And, [self] + predicates)
case .Or: return Predicate(.And, [self, Predicate(.Or, predicates)])
}
}, mapRight: { compoundPredicate in
switch (compoundPredicate.compounder, compounder) {
case (.And, .And): return Predicate(.And, compoundPredicate.predicates + predicates)
default: return Predicate(.And, [self, Predicate(compounder , predicates)])
}
})
}
/**
Convenience function: This will compound a single Predicate using .Or
*/
public func or(field: String, _ comparison:Comparison, _ value: AnyObject) -> Predicate {
return or(.Or, [(field, comparison, value)])
}
/**
Convenience function. This converts the tuples to Predicates and calls `or` with the predicates
*/
public func or(compounder: Compounder, _ predicates: [(String, Comparison, AnyObject)]) -> Predicate {
return or(compounder, predicates.map{ Predicate($0.0, $0.1, $0.2) })
}
/**
Convenience function. This will call `and` using the .And compounder with the provided predicate
*/
public func or(predicate: Predicate) -> Predicate {
return or(.Or, [predicate])
}
/**
This will compound an array of compounded predicates (using the povide compounder) to this predicate using .Or
*/
public func or(compounder: Compounder, _ predicates: [Predicate]) -> Predicate {
return predicate.map(mapLeft: { singlePredicate in
switch compounder {
case .And: return Predicate(.Or, [self, Predicate(.And, predicates)])
case .Or: return Predicate(.Or, [self] + predicates)
}
}, mapRight: { compoundPredicate in
switch (compoundPredicate.compounder, compounder) {
case (.Or, .Or): return Predicate(.Or, compoundPredicate.predicates + predicates)
default: return Predicate(.Or, [self, Predicate(compounder, predicates)])
}
})
}
/**
This generates a new, equivalent NSPredicate to this Predicate
*/
public func nsPredicate() -> NSPredicate {
return predicateUsingTranslator(nil)
}
/**
This creates a new NSPredicate and uses the provided translator (if any) to transform field names into the object names and their respective values needed to run the NSPredicate against the backing Realm Objects.
*/
internal func predicateUsingTranslator(translator: ((String, AnyObject) -> (String?, AnyObject?))?) -> NSPredicate {
return predicate.map(mapLeft: { singlePredicate in
let translated = translator?(singlePredicate.field, singlePredicate.value)
let fieldName: String = translated?.0 ?? singlePredicate.field
let value: AnyObject = translated?.1 ?? singlePredicate.value
return NSPredicate(format: "%K \(singlePredicate.comparison.rawValue) %@", argumentArray: [fieldName, value])
}, mapRight: { predicates in
let nsPredicates:[NSPredicate] = predicates.predicates.map{ $0.predicateUsingTranslator(translator) }
switch predicates.compounder {
case .And: return NSCompoundPredicate.andPredicateWithSubpredicates(nsPredicates)
case .Or: return NSCompoundPredicate.orPredicateWithSubpredicates(nsPredicates)
}
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment