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

59d401dca1a327705632d1e2e20f00f807dee828
moonrise/Packages/MoonriseKit/Sources/ObservingLog/EquipmentUsage.swift history · blame · raw

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}