mycode/myApp/Haru_Danim/IOS/Core/DayMath.swift
2026-07-07 15:20:53 +09:00

145 lines
5.3 KiB
Swift

//
// 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<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
}
/// `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))
}
}
}