mycode/myApp/HaruDanim/Shared/DayMath.swift
songyc macbook 2ae469d187 perf(1.5-p11): 등가 성능 최적화 — 건강 캐시 인메모리·백필 스위프·게이지 버킷화·위젯 타임라인, 빌드 9
소킹 중 '1.4 대비 버벅임 증가' 보고의 원인 수정 (동작·수치 완전 불변):
- HealthCache/HealthIntervals 인메모리 캐시(세대 카운터 무효화) + workoutKeys 이중 load 제거
- DayMath.calendar 저장 프로퍼티화 (접근마다 Calendar 사본 생성 제거)
- 수면 백필 O(400일×샘플수) → 정렬+포인터 스위프 (퍼즈 3,000회 등가 증명)
- refreshRecent 캐시 기록 배칭 (일수×2회 → 총 2회)
- QuestProgress.spanValue/spanRawValue 하루 버킷화 (게이지의 미래 기록 즉시 반영 스펙 보존)
- 위젯 라이브 타임라인 makeEntry refresh 파라미터 (엔트리 7개×컨테이너 오픈 → 1회)
- 일기 건강 카드 칩 value() 이중 호출 제거

검증: 옛/새 빌드 A/B — 덤프 3종 값 동일·4화면 픽셀 동일, 자가 검증 62/20/27/10
ALL PASS(26.5+18.5), 벌크 1만 건 렌더 정상, 3종 빌드 그린, 카탈로그 무변경.
whats-new-1.5 3언어에 성능 개선 줄 추가.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtZwRRjbtM9pZJDM4FkTq6
2026-08-26 14:23:56 +09:00

156 lines
6.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// DayMath.swift
// Haru_Danim
//
// " " (CLAUDE.md §4.3)
//
import Foundation
/// ( ), / ,
struct DayMath {
let settings: TrackingSettings
/// 1 (1.5(9) ).
/// Calendar.current +firstWeekday
/// , dayKey 3·dayKeys/splitByDay ×
/// . settings .
let calendar: Calendar
init(settings: TrackingSettings = .current()) {
self.settings = settings
self.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<Date> {
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..<end
}
func dayRange(containing date: Date) -> Range<Date> {
dayRange(forKey: dayKey(for: date))
}
// MARK: /
/// `date` ( )
func weekRange(containing date: Date) -> Range<Date> {
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..<dayRange(forKey: lastKey).upperBound
}
/// ' ' (1.4): `days` .
/// (· · )
func rollingRange(endingAtKey key: Date, days: Int) -> Range<Date> {
let startKey = calendar.date(byAdding: .day, value: -(days - 1), to: key) ?? key
return dayRange(forKey: startKey).lowerBound..<dayRange(forKey: key).upperBound
}
/// `date`
func monthRange(containing date: Date) -> Range<Date> {
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..<dayRange(forKey: lastKey).upperBound
}
///
func dayKeys(in range: Range<Date>) -> [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<Date>
}
/// ~
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..<segmentEnd))
cursor = segmentEnd
}
return segments
}
}
// MARK: -
/// / ( )
struct Aggregator {
let math: DayMath
init(math: DayMath = DayMath()) {
self.math = math
}
/// (). `now` .
func seconds(for action: Action, in range: Range<Date>, 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<Date>) -> Int {
action.countEntries
.filter { range.contains($0.timestamp) }
.reduce(0) { $0 + $1.amount }
}
func seconds(for actions: [Action], in range: Range<Date>, now: Date = .now) -> TimeInterval {
actions.reduce(0) { $0 + seconds(for: $1, in: range, now: now) }
}
func count(for actions: [Action], in range: Range<Date>) -> 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))
}
}
}