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>
135 lines
4.6 KiB
Swift
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
|
|
}
|
|
}
|