diff --git a/App/Sources/Extensions/TodoFetchDescriptor+Inits.swift b/App/Sources/Extensions/TodoFetchDescriptor+Inits.swift new file mode 100644 index 0000000..0136396 --- /dev/null +++ b/App/Sources/Extensions/TodoFetchDescriptor+Inits.swift @@ -0,0 +1,54 @@ +import Foundation +import SwiftData + +/// Extension providing convenient initializers for fetching todos by group category. +/// +/// This extension allows creating fetch descriptors that automatically filter and sort todos +/// based on their completion status and due dates according to the specified `TodoGroup`. +extension FetchDescriptor where T == Todo { + + /// Creates a fetch descriptor for todos filtered and sorted by a specific group. + /// + /// This initializer provides a convenient way to fetch todos categorized by their temporal + /// relevance and completion status. Each group applies appropriate predicates and sort orders: + /// + /// - **Completed**: All tasks marked as done, sorted by completion date (most recent first) + /// - **Overdue**: Incomplete tasks with due dates before today, sorted by due date (most recent first) + /// - **Today**: Incomplete tasks due today, sorted by due date (earliest first) + /// - **Upcoming**: Incomplete tasks due after today, sorted by due date (earliest first) + /// + /// - Parameters: + /// - group: The todo group category to filter by. + /// - calendar: The calendar to use for date calculations. Defaults to `.current`. + init( + group: TodoGroup, + calendar: Calendar = .current + ) { + let today = calendar.startOfDay(for: .now) + let tomorrow = calendar.date(byAdding: .day, value: 1, to: today) ?? today + + switch group { + case .completed: + self.init( + predicate: #Predicate { $0.completed != nil }, + sortBy: [.init(\.completed, order: .reverse)] + ) + case .overdue: + self.init( + predicate: #Predicate { $0.completed == nil && $0.due < today }, + sortBy: [.init(\.due, order: .reverse)] + ) + case .today: + self.init( + predicate: #Predicate { $0.completed == nil && $0.due >= today && $0.due < tomorrow }, + sortBy: [.init(\.due, order: .forward)] + ) + case .upcoming: + self.init( + predicate: #Predicate { $0.completed == nil && $0.due >= tomorrow }, + sortBy: [.init(\.due, order: .forward)] + ) + } + } + +} diff --git a/UnitTests/Tests/Extensions/TodoFetchDescriptorTests.swift b/UnitTests/Tests/Extensions/TodoFetchDescriptorTests.swift new file mode 100644 index 0000000..68d6ddf --- /dev/null +++ b/UnitTests/Tests/Extensions/TodoFetchDescriptorTests.swift @@ -0,0 +1,130 @@ +import Foundation +import SwiftData +import Testing + +@testable import Todos + +@Suite("FetchDescriptor extension tests") +struct TodoFetchDescriptorTests { + + // MARK: Initialization tests + + @Test( + "initializes with group when empty tasks", + arguments: TodoGroup.allCases + ) + @MainActor + func initializationWhenEmptyTasks(for group: TodoGroup) throws { + // GIVEN + let container = try modelContainer + let context = container.mainContext + + // WHEN + let descriptor = FetchDescriptor(group: .today) + let results = try context.fetch(descriptor) + + // THEN + #expect(results.isEmpty) + } + + @Test( + "initializes with group when filled tasks", + arguments: TodoGroup.allCases + ) + @MainActor + func initializationWhenFilledTasks(for group: TodoGroup) throws { + // GIVEN + let container = try modelContainer + let context = container.mainContext + + insert(into: context) + + // WHEN + let descriptor = FetchDescriptor(group: group) + let results = try context.fetch(descriptor) + + // THEN + #expect(results.count == 2) + + switch group { + case .completed: + #expect(results[0].title == "Completed task #2") + #expect(results[1].title == "Completed task #1") + case .overdue: + #expect(results[0].title == "Overdue task #2") + #expect(results[1].title == "Overdue task #1") + case .today: + #expect(results[0].title == "Today task #1") + #expect(results[1].title == "Today task #2") + case .upcoming: + #expect(results[0].title == "Upcoming task #1") + #expect(results[1].title == "Upcoming task #2") + } + } + +} + +// MARK: - Helpers + +private extension TodoFetchDescriptorTests { + + // MARK: Computed + + var modelContainer: ModelContainer { + get throws { + try ModelContainer( + for: Todo.self, + configurations: .init(isStoredInMemoryOnly: true) + ) + } + } + + // MARK: Functions + + func insert( + into context: ModelContext, + calendar: Calendar = .current + ) { + let tasks: [Todo] = [ + .init( + title: "Today task #1", + due: .now + ), + .init( + title: "Today task #2", + due: .now + ), + .init( + title: "Upcoming task #1", + due: calendar.date(byAdding: .day, value: 1, to: .now) ?? .now + ), + .init( + title: "Upcoming task #2", + due: calendar.date(byAdding: .day, value: 1, to: .now) ?? .now + ), + .init( + title: "Overdue task #1", + due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now + ), + .init( + title: "Overdue task #2", + due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now + ), + .init( + title: "Completed task #1", + due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now, + completed: .now + ), + .init( + title: "Completed task #2", + due: calendar.date(byAdding: .day, value: 1, to: .now) ?? .now, + completed: .now + ) + ] + + for task in tasks { + context.insert(task) + } + } + +}