krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

Packages/MoonriseKit/Sources/ObservingLog/EquipmentUsage.swift

6c4b28384d65e09486e0ad62f2ac9d10401cc80c
moonrise/Packages/MoonriseKit/Sources/ObservingLog/EquipmentUsage.swift history · blame · raw

82 lines · 3101 bytes

 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}