// // DayMath.swift // Haru_Danim // // "하루 시작 시간" 기준의 논리적 하루 계산과 세션 분할 (CLAUDE.md §4.3) // import Foundation /// 논리적 하루(하루 시작 시간 반영), 주/월 범위, 세션 분할 계산 struct DayMath { let settings: TrackingSettings init(settings: TrackingSettings = .current()) { self.settings = settings } var calendar: Calendar { settings.calendar } // MARK: 논리적 하루 /// `date`가 속하는 논리적 하루의 키(해당 달력일의 자정 Date). /// 예: 하루 시작 06:00일 때 7/7 05:00 → 7/6이 키. func dayKey(for date: Date) -> Date { let startOfDay = calendar.startOfDay(for: date) let boundary = calendar.date(byAdding: .minute, value: settings.dayStartMinutes, to: startOfDay)! if date < boundary { return calendar.date(byAdding: .day, value: -1, to: startOfDay)! } return startOfDay } /// 키(자정 Date)가 가리키는 논리적 하루의 실제 시각 범위 func dayRange(forKey key: Date) -> Range { let start = calendar.date(byAdding: .minute, value: settings.dayStartMinutes, to: key)! let nextKey = calendar.date(byAdding: .day, value: 1, to: key)! let end = calendar.date(byAdding: .minute, value: settings.dayStartMinutes, to: nextKey)! return start.. Range { dayRange(forKey: dayKey(for: date)) } // MARK: 주 / 월 범위 /// `date`가 속한 논리적 하루를 포함하는 주(주 시작 요일 설정 반영)의 범위 func weekRange(containing date: Date) -> Range { var key = dayKey(for: date) while calendar.component(.weekday, from: key) != calendar.firstWeekday { key = calendar.date(byAdding: .day, value: -1, to: key)! } let lastKey = calendar.date(byAdding: .day, value: 6, to: key)! return dayRange(forKey: key).lowerBound.. Range { let key = dayKey(for: date) let comps = calendar.dateComponents([.year, .month], from: key) let firstKey = calendar.date(from: comps)! let dayCount = calendar.range(of: .day, in: .month, for: firstKey)!.count let lastKey = calendar.date(byAdding: .day, value: dayCount - 1, to: firstKey)! return dayRange(forKey: firstKey).lowerBound..) -> [Date] { var keys: [Date] = [] var key = dayKey(for: range.lowerBound) while dayRange(forKey: key).lowerBound < range.upperBound { keys.append(key) key = calendar.date(byAdding: .day, value: 1, to: key)! } return keys } // MARK: 세션 분할 (하루 경계 걸침 처리) struct DaySegment { let dayKey: Date let range: Range } /// 시작~종료 구간을 논리적 하루 경계 기준으로 분할 func splitByDay(start: Date, end: Date) -> [DaySegment] { guard start < end else { return [] } var segments: [DaySegment] = [] var cursor = start while cursor < end { let key = dayKey(for: cursor) let dayEnd = dayRange(forKey: key).upperBound let segmentEnd = min(dayEnd, end) segments.append(DaySegment(dayKey: key, range: cursor.., now: Date = .now) -> TimeInterval { action.sessions.reduce(0) { total, session in let end = session.endAt ?? now let overlapStart = max(session.startAt, range.lowerBound) let overlapEnd = min(end, range.upperBound) return total + max(0, overlapEnd.timeIntervalSince(overlapStart)) } } /// 지정 구간 내 행동의 총 횟수 func count(for action: Action, in range: Range) -> Int { action.countEntries .filter { range.contains($0.timestamp) } .reduce(0) { $0 + $1.amount } } func seconds(for actions: [Action], in range: Range, now: Date = .now) -> TimeInterval { actions.reduce(0) { $0 + seconds(for: $1, in: range, now: now) } } func count(for actions: [Action], in range: Range) -> Int { actions.reduce(0) { $0 + count(for: $1, in: range) } } /// 오늘(논리적 하루)의 값 — 메인 버튼 표시용 func todayValue(for action: Action, now: Date = .now) -> Double { let range = math.dayRange(containing: now) switch action.trackingType { case .time: return seconds(for: action, in: range, now: now) case .count: return Double(count(for: action, in: range)) } } }