Files
Reflect/Shared/Persisence/DataControllerGET.swift
Trey t 0442eab1f8 Rebrand entire project from Feels to Reflect
Complete rename across all bundle IDs, App Groups, CloudKit containers,
StoreKit product IDs, data store filenames, URL schemes, logger subsystems,
Swift identifiers, user-facing strings (7 languages), file names, directory
names, Xcode project, schemes, assets, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:47:16 -06:00

135 lines
4.6 KiB
Swift

//
// 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<MoodEntryModel>(
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<MoodEntryModel>(
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
}
}