Packages/MoonriseKit/Sources/ObservingLog/EquipmentUsage.swift
84 lines · 3300 bytes
5 symbols in this file
1import Foundation
2
3/// How one inventory item has been used across the log.
4public struct EquipmentUsage: Sendable, Equatable {
5 public struct Tally: Sendable, Equatable {
6 public var key: String
7 public var count: Int
8 }
9
10 public enum Season: String, Sendable, CaseIterable {
11 case winter, spring, summer, autumn
12 public var title: String { rawValue.capitalized }
13 }
14
15 public var observations: Int
16 public var first: Date?
17 public var last: Date?
18 public var distinctObjects: Int
19 /// Object names, most observed first.
20 public var topObjects: [Tally]
21 /// Observations in each calendar month, January first.
22 public var byMonth: [Int]
23 /// Meteorological seasons for the observer's hemisphere (southern when latitude is negative).
24 public var bySeason: [Season: Int]
25 /// Mean of the observations with seeing noted.
26 public var averageSeeing: Double?
27 /// IDs of other items logged alongside this one, most frequent first.
28 public var usedWith: [Tally]
29
30 /// Usage of the item `id` in `log`. `calendar` decides month and season boundaries. Objects are counted by
31 /// catalogue ID where `resolver` finds one, else by name.
32 public init(of id: String, in log: [Observation], calendar: Calendar = .current, resolver: CatalogResolver = .messier) {
33 let mine = log.filter { $0.equipmentIDs.contains(id) }
34 observations = mine.count
35 first = mine.map(\.date).min()
36 last = mine.map(\.date).max()
37
38 var objects: [String: (name: String, count: Int)] = [:]
39 for o in mine {
40 let name = o.object.trimmingCharacters(in: .whitespaces)
41 let key = resolver.keys(for: o)[0]
42 objects[key, default: (key == CatalogResolver.fold(name) ? name : key, 0)].count += 1
43 }
44 distinctObjects = objects.count
45 topObjects = objects.values.map { Tally(key: $0.name, count: $0.count) }.sorted(by: Self.order)
46
47 var months = Array(repeating: 0, count: 12)
48 var seasons: [Season: Int] = [:]
49 for o in mine {
50 let m = calendar.component(.month, from: o.date)
51 months[m - 1] += 1
52 seasons[Self.season(month: m, southern: (o.latitude ?? 0) < 0), default: 0] += 1
53 }
54 byMonth = months
55 bySeason = seasons
56
57 let seeing = mine.compactMap(\.seeing)
58 averageSeeing = seeing.isEmpty ? nil : Double(seeing.reduce(0, +)) / Double(seeing.count)
59
60 var others: [String: Int] = [:]
61 for o in mine { for other in Set(o.equipmentIDs) where other != id { others[other, default: 0] += 1 } }
62 usedWith = others.map { Tally(key: $0.key, count: $0.value) }.sorted(by: Self.order)
63 }
64
65 static func order(_ a: Tally, _ b: Tally) -> Bool {
66 a.count != b.count ? a.count > b.count : a.key < b.key
67 }
68
69 static func season(month: Int, southern: Bool) -> Season {
70 let north: Season = switch month {
71 case 12, 1, 2: .winter
72 case 3...5: .spring
73 case 6...8: .summer
74 default: .autumn
75 }
76 guard southern else { return north }
77 return switch north {
78 case .winter: .summer
79 case .spring: .autumn
80 case .summer: .winter
81 case .autumn: .spring
82 }
83 }
84}