Skip to content

Commit fff8b7b

Browse files
authored
Merge pull request #33 from PureSwift/feature/predicate
Add support for Foundation.SortDescriptor
2 parents ab24058 + 635e2a0 commit fff8b7b

4 files changed

Lines changed: 243 additions & 8 deletions

File tree

‎Sources/CoreDataModel/NSFetchRequest.swift‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,9 @@ public extension FetchRequest {
2323
fetchRequest.predicate = predicate?.toFoundation()
2424
fetchRequest.fetchLimit = fetchLimit
2525
fetchRequest.fetchOffset = fetchOffset
26-
var sortDescriptors = sortDescriptors.compactMap { sort -> NSSortDescriptor? in
27-
guard let property = sort.property else {
28-
// Function-based sort terms are not supported by NSFetchRequest;
29-
// they require in-memory evaluation, not yet implemented.
30-
return nil
31-
}
32-
return NSSortDescriptor(key: property.rawValue, ascending: sort.ascending)
33-
}
26+
// Function-based sort terms are not supported by NSFetchRequest;
27+
// they require in-memory evaluation, not yet implemented.
28+
var sortDescriptors = sortDescriptors.compactMap { $0.toFoundation() }
3429
sortDescriptors.append(NSSortDescriptor(key: NSManagedObject.BuiltInProperty.id.rawValue, ascending: true))
3530
fetchRequest.sortDescriptors = sortDescriptors
3631
let resultType: NSFetchRequestResultType

‎Sources/CoreModel/FetchRequest.swift‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,30 @@ public struct FetchRequest: Equatable, Hashable, Sendable {
3838
#if !hasFeature(Embedded)
3939
extension FetchRequest: Codable {}
4040
#endif
41+
42+
// MARK: - Foundation
43+
44+
#if canImport(Darwin)
45+
import Foundation
46+
47+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
48+
public extension FetchRequest {
49+
50+
/// Creates a fetch request sorted with ``Foundation.SortDescriptor`` values.
51+
init<Root: NSObject>(
52+
entity: EntityName,
53+
sortDescriptors: [Foundation.SortDescriptor<Root>],
54+
predicate: Predicate? = nil,
55+
fetchLimit: Int = 0,
56+
fetchOffset: Int = 0
57+
) {
58+
self.init(
59+
entity: entity,
60+
sortDescriptors: sortDescriptors.map { SortDescriptor($0) },
61+
predicate: predicate,
62+
fetchLimit: fetchLimit,
63+
fetchOffset: fetchOffset
64+
)
65+
}
66+
}
67+
#endif

‎Sources/CoreModel/SortDescriptor.swift‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,67 @@ extension FetchRequest.SortDescriptor: Codable {}
4848
extension FetchRequest.SortTerm: Codable {}
4949
#endif
5050

51+
// MARK: - SortComparator
52+
53+
#if canImport(FoundationEssentials)
54+
import FoundationEssentials
55+
#elseif canImport(Foundation)
56+
import Foundation
57+
#endif
58+
59+
#if canImport(FoundationEssentials) || canImport(Foundation)
60+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
61+
public extension FetchRequest.SortDescriptor {
62+
63+
/// The sort order (`.forward` when ascending).
64+
var order: SortOrder {
65+
get { ascending ? .forward : .reverse }
66+
set { ascending = newValue == .forward }
67+
}
68+
69+
init(term: FetchRequest.SortTerm, order: SortOrder) {
70+
self.init(term: term, ascending: order == .forward)
71+
}
72+
73+
init(property: PropertyKey, order: SortOrder) {
74+
self.init(property: property, ascending: order == .forward)
75+
}
76+
}
77+
78+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
79+
extension FetchRequest.SortDescriptor: SortComparator {
80+
81+
/// Compares two model objects using this descriptor's sort term and order.
82+
///
83+
/// Function-based sort terms have no registered functions available through
84+
/// `SortComparator`, so they evaluate to `nil` and compare as equal;
85+
/// use `Array.sorted(by:functions:)` to sort with custom functions.
86+
public func compare(_ lhs: ModelData, _ rhs: ModelData) -> ComparisonResult {
87+
let lhsValue: AttributeValue?
88+
let rhsValue: AttributeValue?
89+
switch term {
90+
case let .property(property):
91+
lhsValue = lhs.attributes[property]
92+
rhsValue = rhs.attributes[property]
93+
case let .function(function):
94+
let expression = FetchRequest.Predicate.Expression.function(function)
95+
lhsValue = expression.evaluate(with: lhs, functions: [:])?.attributeValue
96+
rhsValue = expression.evaluate(with: rhs, functions: [:])?.attributeValue
97+
}
98+
guard let comparison = AttributeValue.order(lhsValue, rhsValue), comparison != 0 else {
99+
return .orderedSame
100+
}
101+
let ascendingResult: ComparisonResult = comparison < 0 ? .orderedAscending : .orderedDescending
102+
switch order {
103+
case .forward:
104+
return ascendingResult
105+
case .reverse:
106+
return ascendingResult == .orderedAscending ? .orderedDescending : .orderedAscending
107+
}
108+
}
109+
}
110+
#endif
111+
51112
// MARK: - Foundation
52113

53114
#if canImport(Darwin)
@@ -62,5 +123,38 @@ public extension FetchRequest.SortDescriptor {
62123
self.term = .property(PropertyKey(rawValue: sortDescriptor.key ?? ""))
63124
self.ascending = sortDescriptor.ascending
64125
}
126+
127+
/// Converts to a ``Foundation.SortDescriptor`` comparing the specified root type.
128+
///
129+
/// Returns `nil` for function-based sort terms, which have no Foundation equivalent.
130+
func toFoundation<Root: NSObject>(comparing root: Root.Type) -> Foundation.SortDescriptor<Root>? {
131+
guard let sortDescriptor = toFoundation() else {
132+
return nil
133+
}
134+
return Foundation.SortDescriptor(sortDescriptor, comparing: root)
135+
}
136+
}
137+
138+
public extension FetchRequest.SortDescriptor {
139+
140+
/// Creates a ``FetchRequest.SortDescriptor`` from an `NSSortDescriptor`.
141+
///
142+
/// Returns `nil` if the sort descriptor has no key path.
143+
init?(_ sortDescriptor: NSSortDescriptor) {
144+
guard let key = sortDescriptor.key else {
145+
return nil
146+
}
147+
self.init(property: PropertyKey(rawValue: key), ascending: sortDescriptor.ascending)
148+
}
149+
150+
/// Converts to an `NSSortDescriptor`.
151+
///
152+
/// Returns `nil` for function-based sort terms, which have no Foundation equivalent.
153+
func toFoundation() -> NSSortDescriptor? {
154+
guard let property else {
155+
return nil
156+
}
157+
return NSSortDescriptor(key: property.rawValue, ascending: ascending)
158+
}
65159
}
66160
#endif

‎Tests/CoreModelTests/SortDescriptorTests.swift‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,124 @@ import Testing
6161
#expect(sortedEvents.map(\.id) == [1, 2, 3])
6262
#expect(sortDescriptor == FetchRequest.SortDescriptor(sort))
6363
}
64+
65+
@Test func foundationConversion() throws {
66+
67+
let sortDescriptor = FetchRequest.SortDescriptor(
68+
property: PropertyKey(EventObject.CodingKeys.id),
69+
ascending: false
70+
)
71+
let foundationSort = try #require(sortDescriptor.toFoundation(comparing: EventObject.self))
72+
#expect(FetchRequest.SortDescriptor(foundationSort) == sortDescriptor)
73+
74+
let events = [
75+
EventObject(id: 1, name: "Event 1", start: Date(timeIntervalSince1970: 0), speakers: []),
76+
EventObject(id: 3, name: "Event 3", start: Date(timeIntervalSince1970: 60), speakers: []),
77+
EventObject(id: 2, name: "Event 2", start: Date(timeIntervalSince1970: 30), speakers: [])
78+
]
79+
let sortedEvents = events.sorted(using: foundationSort)
80+
#expect(sortedEvents.map(\.id) == [3, 2, 1])
81+
}
82+
83+
@Test func nsSortDescriptor() throws {
84+
85+
let sortDescriptor = FetchRequest.SortDescriptor(
86+
property: PropertyKey(EventObject.CodingKeys.name),
87+
ascending: true
88+
)
89+
let nsSortDescriptor = try #require(sortDescriptor.toFoundation())
90+
#expect(nsSortDescriptor.key == "name")
91+
#expect(nsSortDescriptor.ascending)
92+
#expect(FetchRequest.SortDescriptor(nsSortDescriptor) == sortDescriptor)
93+
94+
// sort descriptors without a key path cannot be converted
95+
let comparatorSortDescriptor = NSSortDescriptor(key: nil, ascending: true)
96+
#expect(FetchRequest.SortDescriptor(comparatorSortDescriptor) == nil)
97+
}
98+
99+
@Test func functionSortTerm() {
100+
101+
// function-based sort terms have no Foundation equivalent
102+
let sortDescriptor = FetchRequest.SortDescriptor(
103+
term: .function(.init(name: "custom", arguments: [.keyPath("name")])),
104+
ascending: true
105+
)
106+
#expect(sortDescriptor.toFoundation() == nil)
107+
#expect(sortDescriptor.toFoundation(comparing: EventObject.self) == nil)
108+
}
109+
110+
@Test func fetchRequest() {
111+
112+
let fetchRequest = FetchRequest(
113+
entity: "Event",
114+
sortDescriptors: [
115+
SortDescriptor(\EventObject.start, order: .forward),
116+
SortDescriptor(\EventObject.id, order: .reverse)
117+
]
118+
)
119+
#expect(fetchRequest.sortDescriptors == [
120+
FetchRequest.SortDescriptor(property: PropertyKey(EventObject.CodingKeys.start), ascending: true),
121+
FetchRequest.SortDescriptor(property: PropertyKey(EventObject.CodingKeys.id), ascending: false)
122+
])
123+
}
124+
}
125+
#endif
126+
127+
#if !canImport(Darwin)
128+
#if canImport(FoundationEssentials)
129+
import FoundationEssentials
130+
#elseif canImport(Foundation)
131+
import Foundation
132+
#endif
133+
import Testing
134+
@testable import CoreModel
135+
#endif
136+
137+
#if canImport(FoundationEssentials) || canImport(Foundation)
138+
@Suite struct SortComparatorTests {
139+
140+
static var events: [ModelData] {
141+
[1, 3, 2].map { id in
142+
ModelData(
143+
entity: "Event",
144+
id: ObjectID(rawValue: id.description),
145+
attributes: [
146+
"id": .int64(numericCast(id)),
147+
"name": .string("Event \(id)")
148+
]
149+
)
150+
}
151+
}
152+
153+
@Test func sortOrder() {
154+
155+
var sortDescriptor = FetchRequest.SortDescriptor(property: "id", order: .forward)
156+
#expect(sortDescriptor.ascending)
157+
#expect(sortDescriptor.order == .forward)
158+
sortDescriptor.order = .reverse
159+
#expect(sortDescriptor.ascending == false)
160+
#expect(sortDescriptor == FetchRequest.SortDescriptor(term: .property("id"), order: .reverse))
161+
}
162+
163+
@Test func sortComparator() {
164+
165+
let events = Self.events
166+
let forward = FetchRequest.SortDescriptor(property: "id", order: .forward)
167+
#expect(events.sorted(using: forward).map(\.id.rawValue) == ["1", "2", "3"])
168+
let reverse = FetchRequest.SortDescriptor(property: "id", order: .reverse)
169+
#expect(events.sorted(using: reverse).map(\.id.rawValue) == ["3", "2", "1"])
170+
171+
let lhs = events[0]
172+
#expect(forward.compare(lhs, lhs) == .orderedSame)
173+
#expect(forward.compare(events[1], events[2]) == .orderedDescending)
174+
#expect(reverse.compare(events[1], events[2]) == .orderedAscending)
175+
176+
// function-based sort terms compare as equal without registered functions
177+
let function = FetchRequest.SortDescriptor(
178+
term: .function(.init(name: "custom", arguments: [.keyPath("name")])),
179+
order: .forward
180+
)
181+
#expect(function.compare(events[0], events[1]) == .orderedSame)
182+
}
64183
}
65184
#endif

0 commit comments

Comments
 (0)