// // DataControllerGET.swift // Reflect // // SwiftData READ operations. // import SwiftData import Foundation import os.log private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.88oakapps.reflect", category: "DataControllerGET") extension DataController { func getEntry(byDate date: Date) -> MoodEntryModel? { let startDate = Calendar.current.startOfDay(for: date) guard let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate) else { logger.error("Failed to calculate end date for getEntry") return nil } var descriptor = FetchDescriptor( predicate: #Predicate { entry in entry.forDate >= startDate && entry.forDate < endDate }, sortBy: [SortDescriptor(\.forDate, order: .forward)] ) descriptor.fetchLimit = 1 do { return try modelContext.fetch(descriptor).first } catch { logger.error("Failed to fetch entry: \(error.localizedDescription)") return nil } } func getData(startDate: Date, endDate: Date, includedDays: [Int]) -> [MoodEntryModel] { let weekDays = includedDays.isEmpty ? [1, 2, 3, 4, 5, 6, 7] : includedDays let descriptor = FetchDescriptor( predicate: #Predicate { entry in entry.forDate >= startDate && entry.forDate <= endDate && weekDays.contains(entry.weekDay) }, sortBy: [SortDescriptor(\.forDate, order: .forward)] ) do { return try modelContext.fetch(descriptor) } catch { logger.error("Failed to fetch entries: \(error.localizedDescription)") return [] } } /// Calculate the current mood streak efficiently using a single batch query. /// Returns a tuple of (streak count, last logged mood if today has entry). func calculateStreak(from votingDate: Date) -> (streak: Int, todaysMood: Mood?) { let calendar = Calendar.current let dayStart = calendar.startOfDay(for: votingDate) // Fetch last 365 days of entries in a single query guard let yearAgo = calendar.date(byAdding: .day, value: -365, to: dayStart) else { return (0, nil) } let endOfVotingDay = calendar.date(byAdding: .day, value: 1, to: dayStart) ?? votingDate let entries = getData(startDate: yearAgo, endDate: endOfVotingDay, includedDays: []) .filter { $0.mood != .missing && $0.mood != .placeholder } guard !entries.isEmpty else { return (0, nil) } // Build a Set of dates that have valid entries for O(1) lookup let datesWithEntries = Set(entries.map { calendar.startOfDay(for: $0.forDate) }) // Check for today's entry let todaysEntry = entries.first { calendar.isDate($0.forDate, inSameDayAs: votingDate) } let todaysMood = todaysEntry?.mood // Calculate streak walking backwards var streak = 0 var checkDate = votingDate // If no entry for current voting date, start counting from previous day if !datesWithEntries.contains(dayStart) { checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate } while true { let checkDayStart = calendar.startOfDay(for: checkDate) if datesWithEntries.contains(checkDayStart) { streak += 1 checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate } else { break } } return (streak, todaysMood) } func splitIntoYearMonth(includedDays: [Int]) -> [Int: [Int: [MoodEntryModel]]] { // Single query to fetch all data - avoid N*12 queries let data = getData( startDate: Date(timeIntervalSince1970: 0), endDate: Date(), includedDays: includedDays ) guard !data.isEmpty else { return [:] } let calendar = Calendar.current // Group entries by year and month in memory (single pass) var result = [Int: [Int: [MoodEntryModel]]]() for entry in data { let year = calendar.component(.year, from: entry.forDate) let month = calendar.component(.month, from: entry.forDate) if result[year] == nil { result[year] = [:] } if result[year]![month] == nil { result[year]![month] = [] } result[year]![month]!.append(entry) } return result } }