Packages/MoonriseKit/Sources/SkyEngine/DailySummary.swift
60 lines · 2775 bytes
3 symbols in this file
1import Foundation
2
3/// The text of the daily summary notification: tonight's Moon, planets, the best satellite pass and the day's events.
4public struct DailySummary: Sendable, Equatable {
5 public var title: String
6 public var body: String
7
8 public static func phaseName(_ angle: Double) -> String {
9 switch angle {
10 case ..<10, 350...: "New moon"
11 case ..<80: "Waxing crescent"
12 case ..<100: "First quarter"
13 case ..<170: "Waxing gibbous"
14 case ..<190: "Full moon"
15 case ..<260: "Waning gibbous"
16 case ..<280: "Last quarter"
17 default: "Waning crescent"
18 }
19 }
20
21 /// Summary for the night following `date`. `passes` and `events` may cover any span; only those
22 /// for this night (passes) and this calendar day or night (events) are used.
23 public static func make(at date: Date, observer: Observer, passes: [SatellitePass], events: [SkyEvent],
24 locale: Locale = .current, timeZone: TimeZone = .current) throws -> DailySummary {
25 let tonight = try Tonight(at: date, from: observer)
26 let time = Date.FormatStyle(date: .omitted, time: .shortened, locale: locale, timeZone: timeZone)
27 var calendar = Calendar(identifier: .gregorian)
28 calendar.timeZone = timeZone
29
30 let lit = Int((tonight.moonIllumination * 100).rounded())
31 let title = "Tonight: \(phaseName(tonight.moonPhase).lowercased().replacingOccurrences(of: " moon", with: "")) Moon, \(lit)% lit"
32
33 var lines: [String] = []
34
35 let todays = events.filter { calendar.isDate($0.date, inSameDayAs: date) || ($0.date >= tonight.start && $0.date <= tonight.end) }
36 if !todays.isEmpty {
37 lines.append(todays.map(\.title).joined(separator: ". ") + ".")
38 }
39
40 let planets = tonight.planets.filter { $0.best != nil }.map { $0.body.rawValue.capitalized }
41 if planets.isEmpty {
42 lines.append("No bright planets above 10° tonight.")
43 } else {
44 lines.append("Up tonight: \(ListFormatter.localizedString(byJoining: planets)).")
45 }
46
47 if let pass = passes
48 .filter({ $0.visible && $0.featured && $0.rise >= tonight.start.addingTimeInterval(-3600) && $0.rise <= tonight.end })
49 .max(by: { $0.maxAltitude < $1.maxAltitude }) {
50 let when = pass.rise.formatted(time) + (calendar.isDate(pass.rise, inSameDayAs: date) ? "" : " tomorrow")
51 lines.append("\(pass.displayName) passes at \(when), \(Int(pass.maxAltitude.rounded()))° high.")
52 }
53
54 if let sunset = tonight.sunset {
55 lines.append("Sunset \(sunset.formatted(time)).")
56 }
57
58 return DailySummary(title: title, body: lines.joined(separator: " "))
59 }
60}