Skip to content

Instantly share code, notes, and snippets.

@harrytwright
Last active April 30, 2022 13:01
Show Gist options
  • Save harrytwright/ebfa20c1277eed2d2ffaccb73c446c65 to your computer and use it in GitHub Desktop.
Save harrytwright/ebfa20c1277eed2d2ffaccb73c446c65 to your computer and use it in GitHub Desktop.
Swift4 KeyPath Predicate
import Foundation
/// Predicatable is the protocol that all predicting objects conform.
public protocol Predicatable: CustomStringConvertible {
/// Returns a Boolean value indicating whether the specified object matches the conditions specified by the predicate.
///
/// - Parameter object: The object against which to evaluate the predicate.
/// - Returns: `true` if object matches the conditions specified by the predicate, otherwise `false`.
func evaluate(with object: Any?) -> Bool
/// The predicate's format string.
var predicateFormat: String { get }
}
extension Predicatable {
public var description: String {
return self.predicateFormat
}
}
extension NSPredicate: Predicatable {
public convenience init(with predicatable: Predicatable) {
self.init(format: predicatable.predicateFormat)
}
}
/// Structure to create a logical test for dates.
///
/// Simply used by passing the keyPath to a date and choosing an event,
/// then using the `evaluate(with:)` the logic will be tested
public struct DatePredicate<Key: Codable>: Predicatable {
/// The date events
public enum Event {
case today, tomorrow, yesterday, sameDay(as: Date)
}
/// The key path for the date
public var keyPath: KeyPath<Key, Date>
// The event to check
public var event: Event
public init(keyPath: KeyPath<Key, Date>, is event: Event) {
self.keyPath = keyPath
self.event = event
}
public func evaluate(with object: Any?) -> Bool {
guard let value = object as? Key else { return false }
switch event {
case .today: return Calendar.current.isDateInToday(value[keyPath: keyPath])
case .tomorrow: return Calendar.current.isDateInTomorrow(value[keyPath: keyPath])
case .yesterday: return Calendar.current.isDateInTomorrow(value[keyPath: keyPath])
case .sameDay(let date): return Calendar.current.isDate(date, inSameDayAs: value[keyPath: keyPath])
}
}
public var predicateFormat: String {
if let kvoString = keyPath._kvcKeyPathString {
return kvoString + " is \(event)"
}
return "\(keyPath) is \(event)"
}
}
/// Structure to create a logical test to see if an ebject exists inside a sequence.
///
/// Simply used by passing the keyPath to a sequence, then
/// using the `evaluate(with:)` the logic will be tested
public struct ContainPredicate<Key: Codable, Value: Sequence>: Predicatable where Value.Element: Equatable {
/// KeyPath for the sequence
public var keyPath: KeyPath<Key, Value>
// The value you wish to check exists
public var value: Value.Element
public init(keyPath: KeyPath<Key, Value>, contains value: Value.Element) {
self.value = value
self.keyPath = keyPath
}
public func evaluate(with object: Any?) -> Bool {
guard let value = object as? Key else { return false }
return value[keyPath: keyPath].contains { $0 == self.value }
}
public var predicateFormat: String {
if let kvoString = keyPath._kvcKeyPathString {
return kvoString + " contains \(value)"
}
return "\(keyPath) contains \(value)"
}
}
/// Structure to create a logical test to see if an ebject exists inside a range of objects.
///
/// Simply used by passing the keyPath to a value, then
/// using the `evaluate(with:)` the logic will be tested
public struct RangePredicate<Key: Codable, Value: Comparable>: Predicatable {
/// KeyPath for the object
public var keyPath: KeyPath<Key, Value>
/// The range the object should be in
public var range: ClosedRange<Value>
public init(keyPath: KeyPath<Key, Value>, in range: ClosedRange<Value>) {
self.range = range
self.keyPath = keyPath
}
public func evaluate(with object: Any?) -> Bool {
guard let value = object as? Key else { return false }
return range.contains(value[keyPath: keyPath])
}
public var predicateFormat: String {
if let kvoString = keyPath._kvcKeyPathString {
return kvoString + " in \(range)"
}
return "\(keyPath) in \(range)"
}
}
/// <#Description#>
public struct ComparablePredicate<Key: Codable, Value: Comparable> : Predicatable {
public enum Event: String {
case eq = "=="
case lt = "<"
case gt = ">"
}
public var keyPath: KeyPath<Key, Value>
public var event: Event
public var object: Value
public init(where keyPath: KeyPath<Key, Value>, is event: String, to object: Value) {
self.init(where: keyPath, is: Event(rawValue: event) ?? .eq, to: object)
}
public init(where keyPath: KeyPath<Key, Value>, is event: Event, to object: Value) {
self.keyPath = keyPath
self.object = object
self.event = event
}
public func evaluate(with object: Any?) -> Bool {
guard let object = object as? Key else { return false }
return Bool(value: object, predicate: self)
}
public var predicateFormat: String {
if let kvoString = keyPath._kvcKeyPathString {
return kvoString + " \(event.rawValue) \"\(object)\""
}
return "\(keyPath) \(event.rawValue) \"\(object)\""
}
}
public func == <Key: Codable, Value: Comparable>(lhs: KeyPath<Key, Value>, rhs: Value) -> ComparablePredicate<Key, Value> {
return ComparablePredicate<Key, Value>(where: lhs, is: .eq, to: rhs)
}
public func >>= <Key: Codable, Value>(lhs: KeyPath<Key, Value>, rhs: ClosedRange<Value>) -> RangePredicate<Key, Value> {
return RangePredicate<Key, Value>(keyPath: lhs, in: rhs)
}
extension Bool {
init<Element, Value>(value: Element, predicate: (ComparablePredicate<Element, Value>)) {
switch predicate.event {
case .eq:
self = value[keyPath: predicate.keyPath] == predicate.object
case .gt:
self = value[keyPath: predicate.keyPath] > predicate.object
case .lt:
self = value[keyPath: predicate.keyPath] < predicate.object
}
}
}
func check<Key>(_ value: Key, where predicate: [Predicatable]) -> Bool {
var passes: Bool = true
for check in predicate {
if passes == false { return false }
passes = check.evaluate(with: value)
}
return passes
}
public extension Sequence {
public func filter(where predicate: Predicatable...) -> [Element] {
return self.filter(where: predicate)
}
public func filter(where predicate: [Predicatable]) -> [Element] {
return self.filter ({ check($0, where: predicate) })
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment