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/Tests/FeedsTests/SunSummaryTests.swift

6c4b28384d65e09486e0ad62f2ac9d10401cc80c
moonrise/Packages/MoonriseKit/Tests/FeedsTests/SunSummaryTests.swift history · blame · raw

121 lines · 5512 bytes

  1import Foundation
  2import Testing
  3@testable import Feeds
  4
  5private func at(_ s: String) -> Date { try! Date(s, strategy: .iso8601) }
  6
  7private let now = at("2026-10-19T15:00:00Z")
  8private let night = DateInterval(start: at("2026-10-19T23:00:00Z"), end: at("2026-10-20T11:00:00Z"))
  9/// Chicago is at about 50.7° geomagnetic.
 10private let chicago = 50.7
 11
 12private func scales(r: Int = 0, s: Int = 0, g: Int = 0) -> NOAAScales {
 13    NOAAScales(asOf: at("2026-10-19T14:55:00Z"), current: .init(r: r, s: s, g: g), past24h: .init(r: r, s: s, g: g))
 14}
 15
 16private let probabilities = SolarProbabilities(
 17    issued: at("2026-10-19T00:00:00Z"),
 18    days: [.init(c: 85, m: 25, x: 5, proton: 5), .init(c: 80, m: 20, x: 5, proton: 5), .init(c: 80, m: 20, x: 5, proton: 5)]
 19)
 20
 21private let quietKp = [
 22    KpBar(start: at("2026-10-19T12:00:00Z"), kp: 2.33, predicted: false),
 23    KpBar(start: at("2026-10-20T00:00:00Z"), kp: 3.0, predicted: true),
 24]
 25
 26private let quietXRay = [XRaySample(time: at("2026-10-19T14:00:00Z"), flux: 3e-6)]
 27
 28private func summary(
 29    scales s: NOAAScales? = scales(), kp: [KpBar] = quietKp, xray: [XRaySample] = quietXRay,
 30    latitude: Double = chicago, night n: DateInterval = night, at date: Date = now
 31) -> String? {
 32    SunSummary.text(
 33        scales: s, kp: kp, probabilities: probabilities, xray: xray,
 34        geomagneticLatitude: latitude, night: n, at: date,
 35        timeZone: TimeZone(identifier: "UTC")!, locale: Locale(identifier: "en_GB")
 36    )
 37}
 38
 39@Test func quietNamesTodaysFlareChance() {
 40    #expect(summary() == "Quiet. 25% chance of an M-class flare today.")
 41}
 42
 43@Test func quietWithoutProbabilities() {
 44    let text = SunSummary.text(
 45        scales: scales(), kp: quietKp, probabilities: nil, xray: quietXRay,
 46        geomagneticLatitude: chicago, night: night, at: now,
 47        timeZone: TimeZone(identifier: "UTC")!, locale: Locale(identifier: "en_GB")
 48    )
 49    #expect(text == "Quiet. No storms, and no flare of class M or above in the last 6 hours.")
 50}
 51
 52@Test func nothingLoadedGivesNoSummary() {
 53    #expect(summary(scales: nil, kp: [], xray: []) == nil)
 54}
 55
 56@Test func stormForecastAfterDarkAtYourLatitude() {
 57    let kp = quietKp + [KpBar(start: at("2026-10-20T03:00:00Z"), kp: 5.33, predicted: true)]
 58    #expect(summary(kp: kp) == "Minor storm (G1) forecast for 3:00–6:00, Kp 5.3. Aurora possible low in the northern sky.")
 59}
 60
 61@Test func stormNowAndDark() {
 62    let kp = [KpBar(start: at("2026-10-20T00:00:00Z"), kp: 6.67, predicted: true)]
 63    let text = summary(scales: scales(g: 2), kp: kp, at: at("2026-10-20T01:00:00Z"))
 64    #expect(text == "Moderate storm (G2) now, Kp 6.7. Aurora possible low in the northern sky.")
 65}
 66
 67@Test func stormUnderwayBeforeDarkRunsIntoTheNight() {
 68    let kp = [KpBar(start: at("2026-10-19T21:00:00Z"), kp: 5.0, predicted: true)]
 69    let text = summary(kp: kp, at: at("2026-10-19T21:30:00Z"))
 70    #expect(text == "Minor storm (G1) forecast until 0:00, Kp 5.0. Aurora possible low in the northern sky.")
 71}
 72
 73@Test func southernObserverLooksSouth() {
 74    let kp = [KpBar(start: at("2026-10-20T03:00:00Z"), kp: 5.33, predicted: true)]
 75    #expect(summary(kp: kp, latitude: -55)?.hasSuffix("Aurora possible low in the southern sky.") == true)
 76}
 77
 78@Test func stormTooFarFromThePoleIsASecondClause() {
 79    // At 40° geomagnetic, Kp 5.3 does not reach: the oval's visible edge is at 49.6°.
 80    let kp = quietKp + [KpBar(start: at("2026-10-20T03:00:00Z"), kp: 5.33, predicted: true)]
 81    #expect(summary(kp: kp, latitude: 40) == "Quiet now. Minor storm (G1) forecast from 3:00, Kp 5.3.")
 82}
 83
 84@Test func daytimeStormIsASecondClause() {
 85    let kp = quietKp + [KpBar(start: at("2026-10-19T18:00:00Z"), kp: 5.33, predicted: true)]
 86    #expect(summary(kp: kp) == "Quiet now. Minor storm (G1) forecast from 18:00, Kp 5.3.")
 87}
 88
 89@Test func stormNowOutOfReach() {
 90    #expect(summary(scales: scales(g: 1), latitude: 40) == "Minor storm (G1) as of 14:55. Aurora at your latitude would need more than Kp 9.")
 91    #expect(summary(scales: scales(g: 1), latitude: 45) == "Minor storm (G1) as of 14:55. Aurora at your latitude needs Kp 7.6.")
 92}
 93
 94@Test func stormNowInReachButOverBeforeDark() {
 95    #expect(summary(scales: scales(g: 1)) == "Minor storm (G1) as of 14:55. Kp is forecast below 5 after dark.")
 96}
 97
 98@Test func radiationStorm() {
 99    #expect(summary(scales: scales(s: 1)) == "Minor radiation storm (S1) as of 14:55. It affects polar flights and satellites, not people on the ground.")
100}
101
102@Test func recentFlare() {
103    let xray = quietXRay + [XRaySample(time: at("2026-10-19T14:05:00Z"), flux: 1.2e-4)]
104    #expect(summary(xray: xray) == "Strong flare (X1.2) at 14:05. Brief radio blackout on Earth's day side.")
105}
106
107@Test func flareOlderThanSixHoursIsQuiet() {
108    let xray = quietXRay + [XRaySample(time: at("2026-10-19T08:30:00Z"), flux: 2e-5)]
109    #expect(summary(xray: xray) == "Quiet. 25% chance of an M-class flare today.")
110}
111
112@Test func radioBlackoutWithoutXRayData() {
113    #expect(summary(scales: scales(r: 3), xray: []) == "Strong radio blackout (R3) as of 14:55. It affects radio on Earth's day side.")
114}
115
116@Test func stormOutranksRadiationAndFlare() {
117    let xray = [XRaySample(time: at("2026-10-19T14:05:00Z"), flux: 5e-5)]
118    #expect(summary(scales: scales(r: 2, s: 1, g: 1), xray: xray)?.hasPrefix("Minor storm (G1)") == true)
119    #expect(summary(scales: scales(r: 2, s: 1), xray: xray)?.hasPrefix("Minor radiation storm (S1)") == true)
120    #expect(summary(scales: scales(r: 2), xray: xray)?.hasPrefix("Moderate flare (M5.0)") == true)
121}