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/SkyEngineTests/DarknessTests.swift

b327e0d6e7ab67511c5bade3f3c3a93834f6e9ca
moonrise/Packages/MoonriseKit/Tests/SkyEngineTests/DarknessTests.swift history · blame · raw

91 lines · 4704 bytes

 1import Foundation
 2import Testing
 3@testable import SkyEngine
 4
 5private let central = TimeZone(identifier: "America/Chicago")!
 6private let hm = Date.FormatStyle(date: .omitted, time: .shortened, locale: Locale(identifier: "en_GB"), timeZone: central)
 7
 8@Test func astronomicalDuskAndDawnInChicago() throws {
 9    // NOAA solar position algorithm: Sun at −18° at 00:38:30 and 10:33:40 UT on 18 October 2026.
10    let t = try Tonight(at: ut(18, 0), from: chicago)
11    let dark = try #require(t.dark)
12    #expect(abs(dark.start.timeIntervalSince(ut(24, 38) + 30)) < 60)
13    #expect(abs(dark.end.timeIntervalSince(ut(34, 33) + 40)) < 60)
14}
15
16@Test func alreadyDarkStartsNow() throws {
17    // 21:00 CDT; NOAA has the Sun back at −18° at 10:32:30 UT.
18    let t = try Tonight(at: observed, from: chicago)
19    let dark = try #require(t.dark)
20    #expect(dark.start == observed)
21    #expect(abs(dark.end.timeIntervalSince(ut(10, 32) + 30)) < 60)
22}
23
24@Test func noDarknessInTromsoInJune() throws {
25    let tromso = Observer(latitude: 69.65, longitude: 18.96)
26    #expect(try SkyEngine.darkness(from: utc("2026-06-21T12:00:00Z"), to: utc("2026-06-22T12:00:00Z"), observer: tromso) == nil)
27}
28
29@Test func waxingMoonSetsInTheEvening() throws {
30    // 35% lit and up at 21:00 CDT on 16 October 2026; JPL Horizons has it setting at 02:49 UT.
31    let t = try Tonight(at: observed, from: chicago)
32    #expect(t.moonUp.count == 1)
33    #expect(t.moonUp[0].start == t.start)
34    #expect(t.moonrise == nil)
35    let set = try #require(t.moonset)
36    #expect(abs(set.timeIntervalSince(ut(2, 49))) < 90)
37    #expect(t.moonAndDarkness(time: hm) == "Dark 21:00–5:32. Moon 35% lit, sets 21:48.")
38}
39
40@Test func moonUpAlternatesWithRiseAndSet() throws {
41    let start = ut(18, 0)
42    let up = try SkyEngine.moonUp(from: start, to: start + 3 * 86_400, observer: chicago)
43    #expect(up.count >= 3)
44    for (a, b) in zip(up, up.dropFirst()) {
45        #expect(b.start > a.end)
46        #expect(try SkyEngine.horizontal(.moon, at: a.end + b.start.timeIntervalSince(a.end) / 2, from: chicago).altitude < 0)
47    }
48    for i in up {
49        #expect(try SkyEngine.horizontal(.moon, at: i.start + i.duration / 2, from: chicago).altitude > 0)
50    }
51}
52
53@Test func moonAndDarknessText() {
54    let base = ut(24, 0)
55    func at(_ h: Double) -> Date { base + h * 3600 }
56    let night = DateInterval(start: at(-1), end: at(11))
57    let dark = DateInterval(start: at(0.5), end: at(10.5))
58    func text(_ up: [DateInterval], dark: DateInterval? = dark) -> String {
59        SkyEngine.moonAndDarkness(night: night, dark: dark, moonUp: up, moonIllumination: 0.624, time: hm)
60    }
61    #expect(text([]) == "Dark 19:30–5:30. Moon down throughout.")
62    #expect(text([DateInterval(start: at(-1), end: at(0.2))]) == "Dark 19:30–5:30. Moon down throughout.")
63    #expect(text([night]) == "Dark 19:30–5:30. Moon 62% lit, up throughout.")
64    #expect(text([DateInterval(start: at(-1), end: at(4))]) == "Dark 19:30–5:30. Moon 62% lit, sets 23:00.")
65    #expect(text([DateInterval(start: at(8), end: at(11))]) == "Dark 19:30–5:30. Moon 62% lit, rises 3:00.")
66    #expect(text([DateInterval(start: at(-1), end: at(2)), DateInterval(start: at(9), end: at(11))])
67        == "Dark 19:30–5:30. Moon 62% lit, sets 21:00, rises 4:00.")
68    #expect(text([DateInterval(start: at(-1), end: at(0))], dark: nil) == "The sky does not get fully dark. Moon 62% lit, sets 19:00.")
69}
70
71@Test func moonNoteOnFaintPasses() {
72    func pass(_ name: String, peak: Date, visible: Bool = true) -> SatellitePass {
73        SatellitePass(name: name, rise: peak - 180, set: peak + 180, peak: peak, maxAltitude: 60, riseAzimuth: 240, setAzimuth: 60, visible: visible)
74    }
75    // Moon 35% lit and up at 02:00 UT on 17 October 2026, set by 04:00.
76    #expect(pass("HST", peak: observed).moonNote(from: chicago) == "Moon up, 35% lit")
77    #expect(pass("SL-8 R/B", peak: observed).moonNote(from: chicago) == "Moon up, 35% lit")
78    #expect(pass("HST", peak: ut(4, 0)).moonNote(from: chicago) == nil)
79    #expect(pass("HST", peak: observed, visible: false).moonNote(from: chicago) == nil)
80    #expect(pass("ISS (ZARYA)", peak: observed).moonNote(from: chicago) == nil)
81    #expect(pass("CSS (TIANHE)", peak: observed).moonNote(from: chicago) == nil)
82    // 2026-10-12 23:30 UT: 6% lit crescent, 3° up after sunset.
83    #expect(pass("HST", peak: utc("2026-10-12T23:30:00Z")).moonNote(from: chicago) == nil)
84}
85
86@Test func moonlitWhenUpAndLitDuringDarkness() throws {
87    // 35% lit and up until 21:48 CDT.
88    #expect(try Tonight(at: observed, from: chicago).moonlit)
89    // New moon on 12 August 2026.
90    #expect(try !Tonight(at: utc("2026-08-12T18:00:00Z"), from: chicago).moonlit)
91}