102 lines
3.4 KiB
Swift
102 lines
3.4 KiB
Swift
import Foundation
|
|
import SwiftData
|
|
|
|
/// Represents a time-bound period of a ritual. Each arc has its own habits and
|
|
/// completion tracking. When a ritual is renewed, a new arc is created while
|
|
/// the old arc's data remains frozen for historical accuracy.
|
|
@Model
|
|
final class RitualArc {
|
|
var id: UUID = UUID()
|
|
var startDate: Date = Date()
|
|
var endDate: Date = Date()
|
|
var arcNumber: Int = 1
|
|
var isActive: Bool = true
|
|
|
|
@Relationship(deleteRule: .cascade)
|
|
var habits: [ArcHabit]? = []
|
|
|
|
@Relationship(inverse: \Ritual.arcs)
|
|
var ritual: Ritual?
|
|
|
|
init(
|
|
id: UUID = UUID(),
|
|
startDate: Date = Date(),
|
|
endDate: Date,
|
|
arcNumber: Int = 1,
|
|
isActive: Bool = true,
|
|
habits: [ArcHabit] = []
|
|
) {
|
|
self.id = id
|
|
self.startDate = startDate
|
|
self.endDate = endDate
|
|
self.arcNumber = arcNumber
|
|
self.isActive = isActive
|
|
self.habits = habits
|
|
}
|
|
|
|
/// Convenience initializer using duration in days.
|
|
convenience init(
|
|
startDate: Date = Date(),
|
|
durationDays: Int,
|
|
arcNumber: Int = 1,
|
|
isActive: Bool = true,
|
|
habits: [ArcHabit] = []
|
|
) {
|
|
let calendar = Calendar.current
|
|
let start = calendar.startOfDay(for: startDate)
|
|
let end = calendar.date(byAdding: .day, value: durationDays - 1, to: start) ?? start
|
|
|
|
self.init(
|
|
startDate: start,
|
|
endDate: end,
|
|
arcNumber: arcNumber,
|
|
isActive: isActive,
|
|
habits: habits
|
|
)
|
|
}
|
|
|
|
/// The number of days in this arc.
|
|
var durationDays: Int {
|
|
let calendar = Calendar.current
|
|
let days = calendar.dateComponents([.day], from: startDate, to: endDate).day ?? 0
|
|
return days + 1
|
|
}
|
|
|
|
/// Checks if a specific date falls within this arc's date range.
|
|
func contains(date: Date) -> Bool {
|
|
let calendar = Calendar.current
|
|
let checkDate = calendar.startOfDay(for: date)
|
|
let start = calendar.startOfDay(for: startDate)
|
|
let end = calendar.startOfDay(for: endDate)
|
|
return checkDate >= start && checkDate <= end
|
|
}
|
|
|
|
/// Returns the day index (1-based) for a given date within this arc.
|
|
func dayIndex(for date: Date) -> Int {
|
|
let calendar = Calendar.current
|
|
let checkDate = calendar.startOfDay(for: date)
|
|
let start = calendar.startOfDay(for: startDate)
|
|
let days = calendar.dateComponents([.day], from: start, to: checkDate).day ?? 0
|
|
return max(1, min(days + 1, durationDays))
|
|
}
|
|
|
|
/// Creates a new arc that continues from this one with the same habits.
|
|
/// - Parameter durationDays: The duration for the new arc (defaults to same as this arc)
|
|
/// - Returns: A new arc with copied habits and incremented arc number
|
|
func createRenewalArc(durationDays: Int? = nil) -> RitualArc {
|
|
let calendar = Calendar.current
|
|
let newStartDate = calendar.date(byAdding: .day, value: 1, to: endDate) ?? Date()
|
|
let newDuration = durationDays ?? self.durationDays
|
|
|
|
let copiedHabits = (habits ?? []).map { $0.copyForNewArc() }
|
|
|
|
return RitualArc(
|
|
startDate: newStartDate,
|
|
durationDays: newDuration,
|
|
arcNumber: arcNumber + 1,
|
|
isActive: true,
|
|
habits: copiedHabits
|
|
)
|
|
}
|
|
}
|