Reputation: 16628
I can create all kinds of predicates fine, but I'm not sure what can I use for implement "ENDSWITH"?
extension Filter {
var predicate: Predicate<WordModel>? {
switch self {
case .all:
.none
case .lettersContaining(let string):
#Predicate<WordModel> {
$0.letters.localizedStandardContains(string)
}
case .lettersStartingWith(let string):
#Predicate<WordModel> {
$0.letters.starts(with: string)
}
case .lettersEndingWith(let string):
#Predicate<WordModel> {
$0.letters.hasSuffix(string) // 🛑
}
}
}
}
This code says "The hasSuffix(_:) function is not supported in this predicate".
I can see in the docs (at PredicateExpressions) that the predicate closure is a builder expression that can contain a fixed set of expressions, I'm just not sure what to use for my use-case.
What do you use to create "ENDSWITH" predicates for a SwiftData @Query
?
Upvotes: 2
Views: 260
Reputation: 63369
At the time of writing, this definitely doesn't exist, and it can't be implemented manually, either.
The rest of my answer below shows my attempt at that, which ultimately hits a show-stopping limitation, right at the end.
It's possible to implement a predicate that supports matching with ends(with:)
behaviour, but with pretty severe limitations.
Firstly, you can see the list of all the functions the #Predicate
macro supports, here.
private var _knownSupportedFunctions: Set<FunctionStructure> = [
FunctionStructure("contains", arguments: [.unlabeled]),
FunctionStructure("contains", arguments: [.closure(labeled: "where")]),
FunctionStructure("allSatisfy", arguments: [.closure(labeled: nil)]),
FunctionStructure("flatMap", arguments: [.closure(labeled: nil)]),
FunctionStructure("filter", arguments: [.closure(labeled: nil)]),
FunctionStructure("subscript", arguments: [.unlabeled]),
FunctionStructure("subscript", arguments: [.unlabeled, "default"]),
FunctionStructure("starts", arguments: ["with"]),
FunctionStructure("min", arguments: []),
FunctionStructure("max", arguments: []),
FunctionStructure("localizedStandardContains", arguments: [.unlabeled]),
FunctionStructure("localizedCompare", arguments: [.unlabeled]),
FunctionStructure("caseInsensitiveCompare", arguments: [.unlabeled])
]
Even if you hand-roll your own custom predicate type that matches items using ends(with:)
behaviour, you won't be able register it in this _knownSupportedFunctions
, so you won't be able to use it with the #Predicate
macro. So usages of the predicate have to be hand-rolled, and won't be pretty.
Also, the resulting hand-rolled predicate can only be used with pure Swift code, but not with e.g. SwiftData or CoreData, because it's not possible to define how this custom predicate should lower to NSPredicate
(which is what is used to ultimately lower it to SQL).
ends(with:)
functionTo my surprise, there's no ends(with:)
counterpart to starts(with:)
.
We can implement one:
extension BidirectionalCollection where Element: Equatable {
// Adapted from https://github.com/apple/swift/blob/d6f9401/stdlib/public/core/SequenceAlgorithms.swift#L281-L286
@inlinable
public func ends<PossibleSuffix: BidirectionalCollection>(
with possibleSuffix: PossibleSuffix
) -> Bool where PossibleSuffix.Element == Element {
return self.ends(with: possibleSuffix, by: ==)
}
// Adapted from https://github.com/apple/swift/blob/d6f9401/stdlib/public/core/SequenceAlgorithms.swift#L235-L252
@inlinable
public func ends<PossibleSuffix: BidirectionalCollection>(
with possibleSuffix: PossibleSuffix,
by areEquivalent: (Element, PossibleSuffix.Element) throws -> Bool
) rethrows -> Bool {
var possibleSuffixIterator = possibleSuffix.reversed().makeIterator()
for e0 in self.reversed() {
if let e1 = possibleSuffixIterator.next() {
if try !areEquivalent(e0, e1) {
return false
}
}
else {
return true
}
}
return possibleSuffixIterator.next() == nil
}
}
PredicateExpression
Next, we'll need to add the various predicate types needed to wrap it. I wrote all these by copying the SequenceStartsWith
-related code and adapting it as needed.
extension PredicateExpressions {
// Adapted from `SequenceStartsWith`
// https://github.com/apple/swift-foundation/blob/c4fa869/Sources/FoundationEssentials/Predicate/Expressions/Sequence.swift#L101-L124
public struct CollectionEndsWith<
Base : PredicateExpression,
Suffix : PredicateExpression
> : PredicateExpression
where
Base.Output : BidirectionalCollection,
Suffix.Output : BidirectionalCollection,
Base.Output.Element == Suffix.Output.Element,
Base.Output.Element : Equatable
{
public typealias Output = Bool
public let base: Base
public let suffix: Suffix
public init(base: Base, suffix: Suffix) {
self.base = base
self.suffix = suffix
}
public func evaluate(_ bindings: PredicateBindings) throws -> Bool {
try base.evaluate(bindings).ends(with: try suffix.evaluate(bindings))
}
}
}
// Adapted from https://github.com/apple/swift-foundation/blob/c4fa869/Sources/FoundationEssentials/Predicate/Expressions/Sequence.swift#L226-L239
@available(FoundationPredicate 0.1, *)
extension PredicateExpressions.CollectionEndsWith : Codable where Base : Codable, Suffix : Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(base)
try container.encode(suffix)
}
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
self.base = try container.decode(Base.self)
self.suffix = try container.decode(Suffix.self)
}
}
// Adapted from https://github.com/apple/swift-foundation/blob/c4fa869/Sources/FoundationEssentials/Predicate/Expressions/Sequence.swift#L174-L175
@available(FoundationPredicate 0.1, *)
extension PredicateExpressions.CollectionEndsWith : StandardPredicateExpression where Base : StandardPredicateExpression, Suffix : StandardPredicateExpression {}
extension PredicateExpressions {
// Adapted from `build_starts`
// https://github.com/apple/swift-foundation/blob/c4fa869/Sources/FoundationEssentials/Predicate/Expressions/Sequence.swift#L139
public static func build_ends<Base, Suffix>(_ base: Base, with suffix: Suffix) -> CollectionEndsWith<Base, Suffix> {
CollectionEndsWith(base: base, suffix: suffix)
}
}
Finally, we'll need to actually put this new predicate to use. Unfortunately, there's no way for us to modify/extend the #Predicate
macro syntax, short of forking it.
What we can do instead, is to use the #Predicate
with a starts(with:)
call to start, e.g.
let people = [
"Alice",
"Alex",
"Bob",
"Charlie",
"Daniel",
].map(Person.init)
let predicate = #Predicate<Person> { person in
person.name.starts(with: "A")
}
We can then use swift -Xfrontend -dump-macro-expansions predicate_demo.swift
to dump the expansion of that macro:
Macro expands to:
let predicate = Foundation.Predicate<Person>({ person in
PredicateExpressions.build_starts(
PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(person),
keyPath: \.name
),
with: PredicateExpressions.build_Arg("A")
)
})
We can then modify the build_starts
to build_ends
, to get our final example:
let predicate = Foundation.Predicate<Person>({ person in
PredicateExpressions.build_ends( // Replaced `.build_starts(`
PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(person),
keyPath: \.name
),
with: PredicateExpressions.build_Arg("e")
)
})
let namesEndingWithE = try people.filter(predicate).map(\.name)
print(namesEndingWithE) // ["Alice", "Charlie"]
NSPredicate
To make this new CollectionEndsWith
predicate work with SwiftData, CoreData, etc., we'll need to implement the ability to lower it to NSPredicate
. It could leverage the existing NSComparisonPredicate.Operator.endsWith
.
Here's what would look like:
// Adapted from https://github.com/apple/swift-foundation/blob/c4fa869/NSPredicateConversion.swift#L471-L475
extension PredicateExpressions.CollectionEndsWith : ConvertibleExpression where Base.Output : StringProtocol, Suffix.Output : StringProtocol {
fileprivate func convert(state: inout NSPredicateConversionState) throws -> ExpressionOrPredicate {
.predicate(NSComparisonPredicate(leftExpression: try base.convertToExpression(state: &state), rightExpression: try suffix.convertToExpression(state: &state), modifier: .direct, type: .endsWith))
}
}
Unfortunately, this isn't possible, because it relies on a bunch of private members from swift-foundation/Sources/FoundationEssentials/Predicate/NSPredicateConversion.swift
, like ConvertibleExpression
.
Upvotes: 2