Packages/MoonriseKit/Sources/ObservingLog/EquipmentUsage.swift
82 lines · 3101 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.
31 public init(of id: String, in log: [Observation], calendar: Calendar = .current) {
32 let mine = log.filter { $0.equipmentIDs.contains(id) }
33 observations = mine.count
34 first = mine.map(\.date).min()
35 last = mine.map(\.date).max()
36
37 var objects: [String: (name: String, count: Int)] = [:]
38 for o in mine {
39 let name = o.object.trimmingCharacters(in: .whitespaces)
40 objects[name.lowercased(), default: (name, 0)].count += 1
41 }
42 distinctObjects = objects.count
43 topObjects = objects.values.map { Tally(key: $0.name, count: $0.count) }.sorted(by: Self.order)
44
45 var months = Array(repeating: 0, count: 12)
46 var seasons: [Season: Int] = [:]
47 for o in mine {
48 let m = calendar.component(.month, from: o.date)
49 months[m - 1] += 1
50 seasons[Self.season(month: m, southern: (o.latitude ?? 0) < 0), default: 0] += 1
51 }
52 byMonth = months
53 bySeason = seasons
54
55 let seeing = mine.compactMap(\.seeing)
56 averageSeeing = seeing.isEmpty ? nil : Double(seeing.reduce(0, +)) / Double(seeing.count)
57
58 var others: [String: Int] = [:]
59 for o in mine { for other in Set(o.equipmentIDs) where other != id { others[other, default: 0] += 1 } }
60 usedWith = others.map { Tally(key: $0.key, count: $0.value) }.sorted(by: Self.order)
61 }
62
63 static func order(_ a: Tally, _ b: Tally) -> Bool {
64 a.count != b.count ? a.count > b.count : a.key < b.key
65 }
66
67 static func season(month: Int, southern: Bool) -> Season {
68 let north: Season = switch month {
69 case 12, 1, 2: .winter
70 case 3...5: .spring
71 case 6...8: .summer
72 default: .autumn
73 }
74 guard southern else { return north }
75 return switch north {
76 case .winter: .summer
77 case .spring: .autumn
78 case .summer: .winter
79 case .autumn: .spring
80 }
81 }
82}