From c736d3e0482e8b845935d8ecdde9ec66149012d3 Mon Sep 17 00:00:00 2001 From: mannylopez Date: Wed, 10 Jul 2024 21:02:28 -0700 Subject: [PATCH 1/9] Replace method to calculate phase with much more precise method --- .../TinyMoon+AstronomicalConstant.swift | 117 +++++++++++++++++- .../AstronomicalConstantTests.swift | 12 +- .../Helpers/MoonTestHelper.swift | 4 +- Tests/TinyMoonTests/TinyMoonTests.swift | 95 +++++++++++--- Tests/TinyMoonTests/UTCTests.swift | 116 ++++++++--------- 5 files changed, 256 insertions(+), 88 deletions(-) diff --git a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift index 1afa99b..9878f9d 100644 --- a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift +++ b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift @@ -122,7 +122,7 @@ extension TinyMoon { cos(s.rightAscension - m.rightAscension)) let illuminatedFraction = (1 + cos(inc)) / 2 - let phase = 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Double.pi + let phase = phase(julianDay: julianDay) return (illuminatedFraction, phase, angle) } @@ -213,3 +213,118 @@ extension TinyMoon { } } } + +extension TinyMoon.AstronomicalConstant { + /* Astronomical constants */ + + static let epoch = 2444238.5 /* 1980 January 0.0 */ + + /* Constants defining the Sun's apparent orbit */ + + static let elonge = 278.833540 /* Ecliptic longitude of the Sun at epoch 1980.0 */ + static let elongp = 282.596403 /* Ecliptic longitude of the Sun at perigee */ + static let eccent = 0.016718 /* Eccentricity of Earth's orbit */ + + /* Elements of the Moon's orbit, epoch 1980.0 */ + + static let mmlong = 64.975464 /* Moon's mean longitude at the epoch */ + static let mmlongp = 349.383063 /* Mean longitude of the perigee at the epoch */ +// static let mlnode = 151.950429 /* Mean longitude of the node at the epoch */ + // let synmonth = 29.53058868 /* Synodic month (new Moon to new Moon) */ + + /* Properties of the Earth */ + + /* Handy mathematical functions */ + + static func fixangle(_ a: Double) -> Double { + return a - 360.0 * floor(a / 360.0) + } + + static func torad(_ d: Double) -> Double { + return d * (Double.pi / 180.0) + } + + static func todeg(_ d: Double) -> Double { + return d * (180.0 / Double.pi) + } + + static func kepler(m: Double, ecc: Double) -> Double { + var e = torad(m) + let mRad = torad(m) + var delta: Double + let maxIterations = 1000 // Set a limit for maximum iterations + var iteration = 0 + let epsilon = 1e-10 // Set a small threshold for convergence + + repeat { + delta = e - ecc * sin(e) - mRad + e -= delta / (1.0 - ecc * cos(e)) + iteration += 1 + if iteration > maxIterations { + print("Warning: Kepler function did not converge") + break + } + } while abs(delta) > epsilon + + return e + } + + static func phase(julianDay: Double) -> Double { + /* Calculation of the Sun's position */ + + let Day = julianDay - epoch /* Date within epoch */ + let N = fixangle((360 / 365.2422) * Day) /* Mean anomaly of the Sun */ + let M = fixangle(N + elonge - elongp) /* Convert from perigee coordinates to epoch 1980.0 */ + var Ec = kepler(m: M, ecc: eccent) /* Solve equation of Kepler */ + Ec = sqrt((1.0 + eccent) / (1.0 - eccent)) * tan(Ec / 2.0) + Ec = 2.0 * todeg(atan(Ec)) /* True anomaly */ + let Lambdasun = fixangle(Ec + elongp) /* Sun's geocentric ecliptic longitude */ + + /* Calculation of the Moon's position */ + + /* Moon's mean longitude */ + let ml = fixangle(13.1763966 * Day + mmlong) + + /* Moon's mean anomaly */ + let MM = fixangle(ml - 0.1114041 * Day - mmlongp) + + /* Evection */ + let Ev = 1.2739 * sin(torad(2.0 * (ml - Lambdasun) - MM)) + + /* Annual equation */ + let Ae = 0.1858 * sin(torad(M)) + + /* Correction term */ + let A3 = 0.37 * sin(torad(M)) + + /* Corrected anomaly */ + let MmP = MM + Ev - Ae - A3 + + /* Correction for the equation of the centre */ + let mEc = 6.2886 * sin(torad(MmP)) + + /* Another correction term */ + let A4 = 0.214 * sin(torad(2.0 * MmP)) + + /* Corrected longitude */ + let lP = ml + Ev + mEc - Ae + A4 + + /* Variation */ + let V = 0.6583 * sin(torad(2.0 * (lP - Lambdasun))) + + /* True longitude */ + let lPP = lP + V + + /* Calculation of the phase of the Moon */ + + /* Age of the Moon in degrees */ + let MoonAge = lPP - Lambdasun + + /* Phase of the Moon */ + // let MoonPhase = (1.0 - cos(torad(MoonAge))) / 2.0 + + // return MoonPhase + // return synmonth * (fixangle(MoonAge) / 360.0) + return fixangle(MoonAge) / 360.0 + } +} diff --git a/Tests/TinyMoonTests/AstronomicalConstantTests.swift b/Tests/TinyMoonTests/AstronomicalConstantTests.swift index 94452f6..939dfe0 100644 --- a/Tests/TinyMoonTests/AstronomicalConstantTests.swift +++ b/Tests/TinyMoonTests/AstronomicalConstantTests.swift @@ -200,7 +200,7 @@ final class AstronomicalConstantTests: XCTestCase { var moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) XCTAssertEqual(moonPhase.illuminatedFraction, 0.9978873506056865) - XCTAssertEqual(moonPhase.phase, 0.48536418607701615) + XCTAssertEqual(moonPhase.phase, 0.49835304181785745) XCTAssertEqual(moonPhase.angle, -2.8703533722710577) // New moon @@ -209,7 +209,7 @@ final class AstronomicalConstantTests: XCTestCase { moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) XCTAssertEqual(moonPhase.illuminatedFraction, 0.007424715413253902) - XCTAssertEqual(moonPhase.phase, 0.02746179502131707) + XCTAssertEqual(moonPhase.phase, 0.019184351732275336) XCTAssertEqual(moonPhase.angle, -1.9356676727903563) // First quarter @@ -218,7 +218,7 @@ final class AstronomicalConstantTests: XCTestCase { moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) XCTAssertEqual(moonPhase.illuminatedFraction, 0.5105081080980992) - XCTAssertEqual(moonPhase.phase, 0.25334508096684466) + XCTAssertEqual(moonPhase.phase, 0.2505449551679033) XCTAssertEqual(moonPhase.angle, -1.2995618398922297) // Last quarter @@ -227,7 +227,11 @@ final class AstronomicalConstantTests: XCTestCase { moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) XCTAssertEqual(moonPhase.illuminatedFraction, 0.5115383513011658) - XCTAssertEqual(moonPhase.phase, 0.7463269026530461) + XCTAssertEqual(moonPhase.phase, 0.7500559090154013) XCTAssertEqual(moonPhase.angle, 1.3632094278875226) } + + func test_phase() { + + } } diff --git a/Tests/TinyMoonTests/Helpers/MoonTestHelper.swift b/Tests/TinyMoonTests/Helpers/MoonTestHelper.swift index 1019dd0..d33d823 100644 --- a/Tests/TinyMoonTests/Helpers/MoonTestHelper.swift +++ b/Tests/TinyMoonTests/Helpers/MoonTestHelper.swift @@ -200,9 +200,9 @@ extension MoonTestHelper { /// *🌗 |23🌘 |24🌘 |25🌘 |26🌘 |27🌘 |28🌘 | /// 29🌘 | *🌑 |31🌒 | /// ``` - static func prettyPrintCalendarForYear(_ year: Int) { + static func prettyPrintCalendarForYear(_ year: Int, timeZone: TimeZone = utcTimeZone) { for month in MonthTestHelper.Month.allCases { - MoonTestHelper.prettyPrintMoonCalendar(month: month, year: year) + MoonTestHelper.prettyPrintMoonCalendar(month: month, year: year, timeZone: timeZone) } } diff --git a/Tests/TinyMoonTests/TinyMoonTests.swift b/Tests/TinyMoonTests/TinyMoonTests.swift index 6a387ce..cbe39b3 100644 --- a/Tests/TinyMoonTests/TinyMoonTests.swift +++ b/Tests/TinyMoonTests/TinyMoonTests.swift @@ -3,9 +3,10 @@ import XCTest final class TinyMoonTests: XCTestCase { - func test_moon_daysUntilFullMoon() { - let utcTimeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) + let utcTimeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) + let pacificTimeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .pacific) + func test_moon_daysUntilFullMoon() { // Full Moon at Jun 22 01:07 var date = TinyMoon.formatDate(year: 2024, month: 06, day: 20) var daysTill = TinyMoon.Moon.daysUntilFullMoon(moonPhase: .newMoon, date: date, timeZone: utcTimeZone) @@ -73,8 +74,6 @@ final class TinyMoonTests: XCTestCase { } func test_moon_daysUntilNewMoon() { - let utcTimeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - // New Moon at May 8 03:21 var date = TinyMoon.formatDate(year: 2024, month: 05, day: 06) var daysTill = TinyMoon.Moon.daysUntilNewMoon( @@ -140,9 +139,7 @@ final class TinyMoonTests: XCTestCase { XCTAssertEqual(daysTill, 29) } - func test_moon_uniquePhases() { - let utcTimeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - + func test_moon_uniquePhases_utcTimeZone() { var emojisByMonth = [MonthTestHelper.Month: Int]() for month in MonthTestHelper.Month.allCases { @@ -171,8 +168,6 @@ final class TinyMoonTests: XCTestCase { } func test_moon_startAndEndOfJulianDay() { - let utcTimeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - var date = TinyMoon.formatDate(year: 2024, month: 01, day: 11, hour: 00, timeZone: utcTimeZone) var (start, end) = TinyMoon.Moon.julianStartAndEndOfDay(date: date, timeZone: utcTimeZone) XCTAssertEqual(start, 2460320.5) @@ -205,22 +200,25 @@ final class TinyMoonTests: XCTestCase { } func test_moon_majorMoonPhaseInRange() throws { - let utcTimeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - - // Full Moon - var date = TinyMoon.formatDate(year: 2024, month: 04, day: 23, timeZone: utcTimeZone) + var date = TinyMoon.formatDate(year: 2024, month: 04, day: 22, timeZone: utcTimeZone) var (start, end) = TinyMoon.Moon.julianStartAndEndOfDay(date: date, timeZone: utcTimeZone) var startMoonPhaseFraction = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: start).phase var endMoonPhaseFraction = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: end).phase XCTAssertNil(TinyMoon.Moon.majorMoonPhaseInRange(start: startMoonPhaseFraction, end: endMoonPhaseFraction)) - date = TinyMoon.formatDate(year: 2024, month: 04, day: 24, timeZone: utcTimeZone) + date = TinyMoon.formatDate(year: 2024, month: 04, day: 23, timeZone: utcTimeZone) (start, end) = TinyMoon.Moon.julianStartAndEndOfDay(date: date, timeZone: utcTimeZone) startMoonPhaseFraction = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: start).phase endMoonPhaseFraction = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: end).phase var fullMoon = try XCTUnwrap(TinyMoon.Moon.majorMoonPhaseInRange(start: startMoonPhaseFraction, end: endMoonPhaseFraction)) XCTAssertEqual(fullMoon, .fullMoon) + date = TinyMoon.formatDate(year: 2024, month: 04, day: 24, timeZone: utcTimeZone) + (start, end) = TinyMoon.Moon.julianStartAndEndOfDay(date: date, timeZone: utcTimeZone) + startMoonPhaseFraction = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: start).phase + endMoonPhaseFraction = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: end).phase + XCTAssertNil(TinyMoon.Moon.majorMoonPhaseInRange(start: startMoonPhaseFraction, end: endMoonPhaseFraction)) + // Full Moon date = TinyMoon.formatDate(year: 2024, month: 01, day: 25, timeZone: utcTimeZone) (start, end) = TinyMoon.Moon.julianStartAndEndOfDay(date: date, timeZone: utcTimeZone) @@ -251,8 +249,6 @@ final class TinyMoonTests: XCTestCase { } func test_moon_dayIncludesMajorMoonPhase() throws { - let utcTimeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - var date = TinyMoon.formatDate(year: 2024, month: 10, day: 17) var possibleMajorPhase = TinyMoon.Moon.dayIncludesMajorMoonPhase( date: date, @@ -272,8 +268,6 @@ final class TinyMoonTests: XCTestCase { } func test_moon_moonPhase() { - let utcTimeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - var date = TinyMoon.formatDate(year: 2024, month: 10, day: 16) var julianDay = TinyMoon.AstronomicalConstant.julianDay(date) var phaseFraction = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay).phase @@ -388,4 +382,69 @@ final class TinyMoonTests: XCTestCase { moon = TinyMoon.calculateMoonPhase(date, timeZone: tokyoTimeZone) XCTAssertNotEqual(moon.moonPhase, .fullMoon) } + + // These following moon phases happen within 5 hours of midnight, so these tests aim to check that the phase is calculated correctly and lands on the correct day + func test_moon_marginOfError() { + // 4 full moon (3 PST / 1 UTC) + // 5 new moon (4 PST / 1 UTC) + // 1 last quarter (1 UTC) + // -------------- + // 10 total (7 PST / 3 UTC) + + // MARK: - PST margin of error tests + // Values from https://www.timeanddate.com/moon/phases/usa/portland-or + + // Full moon on 2/24/2024 @ 04:30 PST + var date = TinyMoon.formatDate(year: 2024, month: 2, day: 24, hour: 4, minute: 30, timeZone: pacificTimeZone) + var moon = TinyMoon.calculateMoonPhase(date, timeZone: pacificTimeZone) + XCTAssertEqual(moon.moonPhase, .fullMoon) + + // New moon on 3/10/2024 @ 01:00 PST + date = TinyMoon.formatDate(year: 2024, month: 3, day: 10, hour: 1, minute: 0, timeZone: pacificTimeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: pacificTimeZone) + XCTAssertEqual(moon.moonPhase, .newMoon) + + // Full moon on 3/25/2024 @ 00:00 PST + date = TinyMoon.formatDate(year: 2024, month: 3, day: 25, hour: 0, minute: 0, timeZone: pacificTimeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: pacificTimeZone) + XCTAssertEqual(moon.moonPhase, .fullMoon) + + // New moon on 3/10/2024 @ 01:00 PST + date = TinyMoon.formatDate(year: 2024, month: 3, day: 10, hour: 1, minute: 0, timeZone: pacificTimeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: pacificTimeZone) + XCTAssertEqual(moon.moonPhase, .newMoon) + + // New moon on 8/4/2024 @ 04:13 PST + date = TinyMoon.formatDate(year: 2024, month: 8, day: 4, hour: 4, minute: 13, timeZone: pacificTimeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: pacificTimeZone) + XCTAssertEqual(moon.moonPhase, .newMoon) + + // New moon on 11/30/2024 @ 22:21 PST + date = TinyMoon.formatDate(year: 2024, month: 11, day: 30, hour: 22, minute: 21, timeZone: pacificTimeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: pacificTimeZone) + XCTAssertEqual(moon.moonPhase, .newMoon) + + // Full moon on 12/15/2024 @ 01:01 PST + date = TinyMoon.formatDate(year: 2024, month: 12, day: 15, hour: 1, minute: 01, timeZone: pacificTimeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: pacificTimeZone) + XCTAssertEqual(moon.moonPhase, .fullMoon) + + // MARK: - UTC margin of error tests + // Values from https://www.timeanddate.com/moon/phases/timezone/utc + + // Last Quarter moon on 4/2/2024 @ 03:14 UTC + date = TinyMoon.formatDate(year: 2024, month: 4, day: 2, hour: 3, minute: 14, timeZone: utcTimeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) + XCTAssertEqual(moon.moonPhase, .lastQuarter) + + // Full moon on 4/23/2024 @ 23:48 UTC + date = TinyMoon.formatDate(year: 2024, month: 4, day: 23, hour: 23, minute: 48, timeZone: utcTimeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) + XCTAssertEqual(moon.moonPhase, .fullMoon) + + // New moon on 9/3/2024 @ 01:55 UTC + date = TinyMoon.formatDate(year: 2024, month: 9, day: 3, hour: 1, minute: 55, timeZone: utcTimeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) + XCTAssertEqual(moon.moonPhase, .newMoon) + } } diff --git a/Tests/TinyMoonTests/UTCTests.swift b/Tests/TinyMoonTests/UTCTests.swift index a68b5f3..5e5f7c1 100644 --- a/Tests/TinyMoonTests/UTCTests.swift +++ b/Tests/TinyMoonTests/UTCTests.swift @@ -5,6 +5,8 @@ import XCTest final class UTCTests: XCTestCase { + let utcTimeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) + // MARK: Internal // MARK: - UTC Tests @@ -18,21 +20,19 @@ final class UTCTests: XCTestCase { var correct = 0.0 var incorrect = 0.0 - let timeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - let newMoonEmoji = TinyMoon.MoonPhase.newMoon.emoji let waningCrescentEmoji = TinyMoon.MoonPhase.waningCrescent.emoji // Returns a New Moon because it falls within this day's 24 hours - var date = TinyMoon.formatDate(year: 2024, month: 09, day: 02, hour: 00, minute: 00) - let moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + var date = TinyMoon.formatDate(year: 2024, month: 09, day: 03, hour: 23, minute: 00) + let moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } // Even though it is the same day, at this exact time, it is not a New Moon - date = TinyMoon.formatDate(year: 2024, month: 09, day: 02, hour: 00, minute: 00) + date = TinyMoon.formatDate(year: 2024, month: 09, day: 03, hour: 23, minute: 00) let exactMoon = TinyMoon.calculateExactMoonPhase(date) XCTAssertNotEqual(exactMoon.exactMoonPhase, .newMoon) XCTAssertNotEqual(exactMoon.exactEmoji, newMoonEmoji) @@ -46,61 +46,59 @@ final class UTCTests: XCTestCase { var correct = 0.0 var incorrect = 0.0 - let timeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - let newMoonEmoji = TinyMoon.MoonPhase.newMoon.emoji var date = TinyMoon.formatDate(year: 2024, month: 01, day: 11) - var moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + var moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 02, day: 09) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 03, day: 10) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 04, day: 08) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 05, day: 08) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 06, day: 06) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 07, day: 05) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 08, day: 04) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) @@ -114,28 +112,28 @@ final class UTCTests: XCTestCase { // if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 10, day: 02) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 11, day: 01) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 12, day: 01) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 12, day: 30) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) XCTAssertEqual(moon.daysTillNewMoon, 0) @@ -150,78 +148,76 @@ final class UTCTests: XCTestCase { var correct = 0.0 var incorrect = 0.0 - let timeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - let firstQuarterEmoji = TinyMoon.MoonPhase.firstQuarter.emoji var date = TinyMoon.formatDate(year: 2024, month: 01, day: 18) - var moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + var moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 02, day: 16) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 03, day: 17) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 04, day: 15) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 05, day: 15) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 06, day: 14) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 07, day: 13) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 08, day: 12) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 09, day: 11) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 10, day: 10) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 11, day: 09) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 12, day: 08) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .firstQuarter) XCTAssertEqual(moon.emoji, firstQuarterEmoji) if moon.emoji == firstQuarterEmoji { correct += 1 } else { incorrect += 1 } @@ -235,8 +231,6 @@ final class UTCTests: XCTestCase { var correct = 0.0 var incorrect = 0.0 - let timeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - let fullMoonEmoji = TinyMoon.MoonPhase.fullMoon.emoji let waxingGibbousEmoji = TinyMoon.MoonPhase.waxingGibbous.emoji @@ -249,7 +243,7 @@ final class UTCTests: XCTestCase { // Although it is the same date and time, since a major phase (Full Moon) occurs within this day's 24 hours, this returns Full Moon date = TinyMoon.formatDate(year: 2024, month: 08, day: 19, hour: 00, minute: 00) - let moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + let moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) @@ -263,26 +257,24 @@ final class UTCTests: XCTestCase { var correct = 0.0 var incorrect = 0.0 - let timeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - let fullMoonEmoji = TinyMoon.MoonPhase.fullMoon.emoji var date = TinyMoon.formatDate(year: 2024, month: 01, day: 25) - var moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + var moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 02, day: 24) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 03, day: 25) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) @@ -296,56 +288,56 @@ final class UTCTests: XCTestCase { // if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 05, day: 23) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 06, day: 22) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 07, day: 21) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 08, day: 19) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 09, day: 18) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 10, day: 17) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 11, day: 15) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 12, day: 15) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) @@ -360,24 +352,22 @@ final class UTCTests: XCTestCase { var correct = 0.0 var incorrect = 0.0 - let timeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) - let lastQuarterEmoji = TinyMoon.MoonPhase.lastQuarter.emoji var date = TinyMoon.formatDate(year: 2024, month: 01, day: 04) - var moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + var moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 02, day: 02) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 03, day: 03) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } @@ -389,55 +379,55 @@ final class UTCTests: XCTestCase { // if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 05, day: 01) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 05, day: 30) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 06, day: 28) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 07, day: 28) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 08, day: 26) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 09, day: 24) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 10, day: 24) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 11, day: 23) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 12, day: 22) - moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .lastQuarter) XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } From 3e4acfdb45282ca2c4f77b3472905c3cdbccbb55 Mon Sep 17 00:00:00 2001 From: mannylopez Date: Thu, 11 Jul 2024 16:13:15 -0700 Subject: [PATCH 2/9] Convert MoonTool source code to Swift --- .../TinyMoon+AstronomicalConstant.swift | 161 +++++++++++++----- 1 file changed, 117 insertions(+), 44 deletions(-) diff --git a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift index 9878f9d..11c455e 100644 --- a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift +++ b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift @@ -92,7 +92,7 @@ extension TinyMoon { return (declination, rightAscension, distance) } - /// Get Moon phase + /// Get Moon phase, composed of illumination, phase, and angle /// /// - Parameters: /// - julianDay: The date in Julian Days @@ -122,7 +122,7 @@ extension TinyMoon { cos(s.rightAscension - m.rightAscension)) let illuminatedFraction = (1 + cos(inc)) / 2 - let phase = phase(julianDay: julianDay) + let phase = _phase(julianDay: julianDay) return (illuminatedFraction, phase, angle) } @@ -206,7 +206,7 @@ extension TinyMoon { /// Formula based on https://github.com/mourner/suncalc/blob/master/suncalc.js#L29 /// and https://github.com/microsoft/AirSim/blob/main/AirLib/include/common/EarthCelestial.hpp#L115 /// - Note - /// - `2440588` is the Julian day for January 1, 1970, 12:00 UTC, aka J170 + /// - `2440588` is the Julian day for January 1, 1970, 12:00 UTC, aka J1970 /// - `1000 * 60 * 60 * 24` is a day in milliseconds static func julianDay(_ date: Date) -> Double { (date.timeIntervalSince1970 * 1000) / (1000 * 60 * 60 * 24) - 0.5 + 2440588.0 @@ -215,42 +215,43 @@ extension TinyMoon { } extension TinyMoon.AstronomicalConstant { - /* Astronomical constants */ + // Julian date on 1 January 1980, 00:00 UTC + static let J1980 = 2444238.5 - static let epoch = 2444238.5 /* 1980 January 0.0 */ + // MARK: - Sun constants - /* Constants defining the Sun's apparent orbit */ + // Ecliptic longitude of the Sun at J1980 + static let sunEclipticLongitudeJ1980 = 278.833540 + // Ecliptic longitude of the Sun's perigee at J1980 + static let sunPerigeeEclipticLongitudeJ1989 = 282.596403 + // Eccentricity of Earth's orbit + static let earthOrbitEccentricity = 0.016718 - static let elonge = 278.833540 /* Ecliptic longitude of the Sun at epoch 1980.0 */ - static let elongp = 282.596403 /* Ecliptic longitude of the Sun at perigee */ - static let eccent = 0.016718 /* Eccentricity of Earth's orbit */ + // MARK: - Moon constants - /* Elements of the Moon's orbit, epoch 1980.0 */ + // Moon's mean longitude at J1980 + static let moonMeanLongitudeJ1980 = 64.975464 + // Longitude of the Moon's perigee at J1980 + static let moonPerigeeLongitudeJ1980 = 349.383063 + // Mean longitude of the node at J1980 + static let moonNodeLongitude1980 = 151.950429 + // Inclination of the Moon's orbit + static let moonOrbitInclination = 5.145396 + // Semi-major axis of Moon's orbit in km + static let moonOrbitSemiMajorAxis = 384401.0 + // Eccentricity of the Moon's orbit + static let moonEccentricity = 0.054900 - static let mmlong = 64.975464 /* Moon's mean longitude at the epoch */ - static let mmlongp = 349.383063 /* Mean longitude of the perigee at the epoch */ -// static let mlnode = 151.950429 /* Mean longitude of the node at the epoch */ - // let synmonth = 29.53058868 /* Synodic month (new Moon to new Moon) */ - /* Properties of the Earth */ - - /* Handy mathematical functions */ + // MARK: - Mathematical formulas static func fixangle(_ a: Double) -> Double { return a - 360.0 * floor(a / 360.0) } - static func torad(_ d: Double) -> Double { - return d * (Double.pi / 180.0) - } - - static func todeg(_ d: Double) -> Double { - return d * (180.0 / Double.pi) - } - static func kepler(m: Double, ecc: Double) -> Double { - var e = torad(m) - let mRad = torad(m) + var e = degreesToRadians(m) + let mRad = degreesToRadians(m) var delta: Double let maxIterations = 1000 // Set a limit for maximum iterations var iteration = 0 @@ -270,47 +271,46 @@ extension TinyMoon.AstronomicalConstant { } static func phase(julianDay: Double) -> Double { - /* Calculation of the Sun's position */ - - let Day = julianDay - epoch /* Date within epoch */ - let N = fixangle((360 / 365.2422) * Day) /* Mean anomaly of the Sun */ - let M = fixangle(N + elonge - elongp) /* Convert from perigee coordinates to epoch 1980.0 */ - var Ec = kepler(m: M, ecc: eccent) /* Solve equation of Kepler */ - Ec = sqrt((1.0 + eccent) / (1.0 - eccent)) * tan(Ec / 2.0) - Ec = 2.0 * todeg(atan(Ec)) /* True anomaly */ - let Lambdasun = fixangle(Ec + elongp) /* Sun's geocentric ecliptic longitude */ + // Calculation of the Sun's position + let jdSinceJ1980 = julianDay - J1980 + let N = fixangle((360 / 365.2422) * jdSinceJ1980) /* Mean anomaly of the Sun */ + let M = fixangle(N + sunEclipticLongitudeJ1980 - sunPerigeeEclipticLongitudeJ1989) /* Convert from perigee coordinates to epoch 1980.0 */ + var Ec = kepler(m: M, ecc: earthOrbitEccentricity) /* Solve equation of Kepler */ + Ec = sqrt((1.0 + earthOrbitEccentricity) / (1.0 - earthOrbitEccentricity)) * tan(Ec / 2.0) + Ec = 2.0 * radiansToDegrees(atan(Ec)) /* True anomaly */ + let Lambdasun = fixangle(Ec + sunPerigeeEclipticLongitudeJ1989) /* Sun's geocentric ecliptic longitude */ /* Calculation of the Moon's position */ /* Moon's mean longitude */ - let ml = fixangle(13.1763966 * Day + mmlong) + let ml = fixangle(13.1763966 * jdSinceJ1980 + moonMeanLongitudeJ1980) /* Moon's mean anomaly */ - let MM = fixangle(ml - 0.1114041 * Day - mmlongp) + let MM = fixangle(ml - 0.1114041 * jdSinceJ1980 - moonPerigeeLongitudeJ1980) /* Evection */ - let Ev = 1.2739 * sin(torad(2.0 * (ml - Lambdasun) - MM)) + let Ev = 1.2739 * sin(degreesToRadians(2.0 * (ml - Lambdasun) - MM)) /* Annual equation */ - let Ae = 0.1858 * sin(torad(M)) + let Ae = 0.1858 * sin(degreesToRadians(M)) /* Correction term */ - let A3 = 0.37 * sin(torad(M)) + let A3 = 0.37 * sin(degreesToRadians(M)) /* Corrected anomaly */ let MmP = MM + Ev - Ae - A3 /* Correction for the equation of the centre */ - let mEc = 6.2886 * sin(torad(MmP)) + let mEc = 6.2886 * sin(degreesToRadians(MmP)) /* Another correction term */ - let A4 = 0.214 * sin(torad(2.0 * MmP)) + let A4 = 0.214 * sin(degreesToRadians(2.0 * MmP)) /* Corrected longitude */ let lP = ml + Ev + mEc - Ae + A4 /* Variation */ - let V = 0.6583 * sin(torad(2.0 * (lP - Lambdasun))) + let V = 0.6583 * sin(degreesToRadians(2.0 * (lP - Lambdasun))) /* True longitude */ let lPP = lP + V @@ -327,4 +327,77 @@ extension TinyMoon.AstronomicalConstant { // return synmonth * (fixangle(MoonAge) / 360.0) return fixangle(MoonAge) / 360.0 } + + /// Calculates the Moon's phase, represented as a fraction + /// + /// - Parameter: + /// - julianDay: The date in Julian Days + /// + /// - Returns: Phase as a percentage of a full circle (i.e., 0 to 1), where 0.0` new moon, `0.25` first quarter, `0.5` full moon, `0.75` last quarter + /// + /// Formula based on source code from https://www.fourmilab.ch/moontoolw/ + static func _phase(julianDay: Double) -> Double { + // Julian days since 1 January 1980, 00:00 UTC + let jdSinceJ1980 = julianDay - J1980 + // Mean anomaly of the Sun + let N = fixangle((360 / 365.2422) * jdSinceJ1980) + // Convert from perigee coordinates to J1980 + let M = fixangle(N + sunEclipticLongitudeJ1980 - sunPerigeeEclipticLongitudeJ1989) + // Solve for Kepler equation + var Ec = kepler(m: M, ecc: earthOrbitEccentricity) + Ec = sqrt((1.0 + earthOrbitEccentricity) / (1.0 - earthOrbitEccentricity)) * tan(Ec / 2.0) + // True anomaly + Ec = 2.0 * radiansToDegrees(atan(Ec)) + // Sun's geocentric ecliptic longitude + let lambdaSun = fixangle(Ec + sunPerigeeEclipticLongitudeJ1989) + + // Calculation of the Moon's position + // Moon's mean longitude + let moonMeanLongitude = fixangle(13.1763966 * jdSinceJ1980 + moonMeanLongitudeJ1980) + // Moon's mean anomaly + let moonMeanAnomaly = fixangle(moonMeanLongitude - 0.1114041 * jdSinceJ1980 - moonPerigeeLongitudeJ1980) + // Moon's ascending node mean longitude + let moonAscendingNodeMeanLongitude = fixangle(moonNodeLongitude1980 - 0.0529539 * jdSinceJ1980) + // Evection + let evection = 1.2739 * sin(degreesToRadians(2 * (moonMeanLongitude - lambdaSun) - moonMeanAnomaly)) + // Annual equation + let annualEquation = 0.1858 * sin(degreesToRadians(M)) + // Corrected term + let A3 = 0.37 * sin(degreesToRadians(M)) + // Corrected anomaly + let MmP = moonMeanAnomaly + evection - annualEquation - A3 + // Correction for the equation of center + let mEc = 6.2886 * sin(degreesToRadians(MmP)) + // Another correction term + let A4 = 0.214 * sin(degreesToRadians(2 * MmP)) + // Corrected longitude + let lP = moonMeanLongitude + evection + mEc - annualEquation + A4 + // Variation + let variation = 0.6583 * sin(degreesToRadians(2 * (lP - lambdaSun))) + // True longitude + let lPP = lP + variation + // Corrected longitude of the node + let NP = moonAscendingNodeMeanLongitude - 0.16 * sin(degreesToRadians(M)) + // Y inclination coordinate + let y = sin(degreesToRadians(lPP - NP)) * cos(degreesToRadians(moonOrbitInclination)) + // X inclination coordinate + let x = cos(degreesToRadians(lPP - NP)) + // Ecliptic longitude + var lambdaMoon = radiansToDegrees(atan2(y, x)) + lambdaMoon += NP + // Ecliptic latitude + let betaM = radiansToDegrees(asin(sin(degreesToRadians(lPP - NP)) * sin(degreesToRadians(moonOrbitInclination)))) + + // Calculation of the phase of the Moon + // Age of the Moon in degrees + let moonAge = lPP - lambdaSun + // Phase of the Moon + let moonPhase = (1 - cos(degreesToRadians(moonAge))) / 2 + // Distance of moon from the center of the Earth + let moonDistance = (moonOrbitSemiMajorAxis * (1 - moonEccentricity * moonEccentricity)) / (1 + moonEccentricity * cos(degreesToRadians(MmP + mEc))) + + // Returns the terminator phase angle as a percentage of a full circle (i.e., 0 to 1) + let normalizedMoonPhase = fixangle(moonAge) / 360.0 + return normalizedMoonPhase + } } From 8fa2e6275609966982b78d2bcd7238043470af51 Mon Sep 17 00:00:00 2001 From: mannylopez Date: Fri, 12 Jul 2024 15:31:28 -0700 Subject: [PATCH 3/9] Complete replace of SunCalc with MoonTool formula. Removed many functions and tests. --- .../TinyMoon+AstronomicalConstant.swift | 468 +++++------------- Sources/TinyMoon/TinyMoon+MoonDetail.swift | 25 + .../AstronomicalConstantTests.swift | 129 +---- 3 files changed, 164 insertions(+), 458 deletions(-) create mode 100644 Sources/TinyMoon/TinyMoon+MoonDetail.swift diff --git a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift index 11c455e..98bafe9 100644 --- a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift +++ b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift @@ -8,188 +8,121 @@ extension TinyMoon { enum AstronomicalConstant { - static let radians = Double.pi / 180 + // MARK: Internal - /// ε Epsilon - /// The obliquity of the ecliptic. Value at the beginning of 2000: - static let e = 23.4397 + /// Julian date on 1 January 1980, 00:00 UTC + static let J1980 = 2444238.5 + /// Eccentricity of Earth's orbit + static let earthOrbitEccentricity = 0.016718 - static let perihelion = 102.9372 + // MARK: - Sun constants - static let astronomicalUnit = 149598000.0 + /// Ecliptic longitude of the Sun at J1980 + static let sunEclipticLongitudeJ1980 = 278.833540 + /// Ecliptic longitude of the Sun's perigee at J1980 + static let sunPerigeeEclipticLongitudeJ1989 = 282.596403 - static func degreesToRadians(_ degrees: Double) -> Double { - degrees * radians - } - - static func radiansToDegrees(_ radians: Double) -> Double { - radians * (180 / Double.pi) - } - - /// δ The declination shows how far the body is from the celestial equator and - /// determines from which parts of the Earth the object can be visible. - /// - /// - Parameters: - /// - longitude: in radians - /// - latitude: in radians - /// - /// - Returns: Declination, in radians - /// - /// Formula based on https://aa.quae.nl/en/reken/hemelpositie.html#1_7 - /// and https://github.com/mourner/suncalc/blob/master/suncalc.js#L39 - /// and https://github.com/microsoft/AirSim/blob/main/AirLib/include/common/EarthCelestial.hpp#L125 - static func declination(longitude: Double, latitude: Double) -> Double { - let e = AstronomicalConstant.degreesToRadians(AstronomicalConstant.e) - return asin(sin(latitude) * cos(e) + cos(latitude) * sin(e) * sin(longitude)) - } - - /// α The right ascension shows how far the body is from the vernal equinox, as measured along the celestial equator - /// - /// - Parameters: - /// - longitude: in radians - /// - latitude: in radians - /// - /// - Returns: Right ascension, in radians - /// - /// Formula based on https://aa.quae.nl/en/reken/hemelpositie.html#1_7 - /// and https://github.com/mourner/suncalc/blob/master/suncalc.js#L38 - /// and https://github.com/microsoft/AirSim/blob/main/AirLib/include/common/EarthCelestial.hpp#L120 - static func rightAscension(longitude: Double, latitude: Double) -> Double { - let e = AstronomicalConstant.degreesToRadians(AstronomicalConstant.e) - return atan2(sin(longitude) * cos(e) - tan(latitude) * sin(e), cos(longitude)) - } - - // MARK: Moon methods - - /// Get the position of the Moon on a given Julian Day - /// - /// - Parameters: - /// - julianDay: The date in Julian Days - /// - /// - Returns: Tuple with δ declination (in radians), α rightAscension (in radians), and distance (in kilometers) - /// - /// Formula based on https://aa.quae.nl/en/reken/hemelpositie.html#4 - /// and https://github.com/microsoft/AirSim/blob/main/AirLib/include/common/EarthCelestial.hpp#L180 - /// and https://github.com/mourner/suncalc/blob/master/suncalc.js#L186 - static func moonCoordinates(julianDay: Double) -> (declination: Double, rightAscension: Double, distance: Double) { - let daysSinceJ2000 = daysSinceJ2000(from: julianDay) - let L = AstronomicalConstant - .degreesToRadians(218.316 + 13.176396 * daysSinceJ2000) // Geocentric ecliptic longitude, in radians - let M = AstronomicalConstant - .degreesToRadians(134.963 + 13.064993 * daysSinceJ2000) // Mean anomaly, in radians - let F = AstronomicalConstant - .degreesToRadians(93.272 + 13.229350 * daysSinceJ2000) // Mean distance of the Moon from its ascending node, in radians - - let longitude = L + AstronomicalConstant - .degreesToRadians(6.289 * sin(M)) // λ Geocentric ecliptic longitude, in radians - let latitude = AstronomicalConstant - .degreesToRadians(5.128 * sin(F)) // φ Geocentric ecliptic latitude, in radians - let distance = 385001 - 20905 * cos(M) // Distance to the Moon, in kilometers - - let declination = declination(longitude: longitude, latitude: latitude) - let rightAscension = rightAscension(longitude: longitude, latitude: latitude) - - return (declination, rightAscension, distance) - } + // MARK: - Moon constants - /// Get Moon phase, composed of illumination, phase, and angle - /// - /// - Parameters: - /// - julianDay: The date in Julian Days - /// - /// - Returns: Tuple containing illuminatedFraction, phase, and angle - /// - /// - illuminatedFraction: Varies between `0.0` new moon and `1.0` full moon - /// - phase: Varies between `0.0` to `0.99`. `0.0` new moon, `0.25` first quarter, `0.5` full moon, `0.75` last quarter - /// - /// Formula based on https://github.com/microsoft/AirSim/blob/main/AirLib/include/common/EarthCelestial.hpp#L89 - /// and https://github.com/mourner/suncalc/blob/master/suncalc.js#L230 - /// and https://github.com/wlandsman/IDLAstro/blob/master/pro/mphase.pro - static func getMoonPhase(julianDay: Double) -> (illuminatedFraction: Double, phase: Double, angle: Double) { - let s = sunCoordinates(julianDay: julianDay) - let m = moonCoordinates(julianDay: julianDay) - - // Geocentric elongation of the Moon from the Sun - let phi = - acos( - sin(s.declination) * sin(m.declination) + cos(s.declination) * cos(m.declination) * - cos(s.rightAscension - m.rightAscension)) - // Selenocentric (Moon centered) elongation of the Earth from the Sun - let inc = atan2(astronomicalUnit * sin(phi), m.distance - astronomicalUnit * cos(phi)) - let angle = atan2( - cos(s.declination) * sin(s.rightAscension - m.rightAscension), - sin(s.declination) * cos(m.declination) - cos(s.declination) * sin(m.declination) * - cos(s.rightAscension - m.rightAscension)) - - let illuminatedFraction = (1 + cos(inc)) / 2 - let phase = _phase(julianDay: julianDay) - - return (illuminatedFraction, phase, angle) - } + /// Moon's mean longitude at J1980 + static let moonMeanLongitudeJ1980 = 64.975464 + /// Longitude of the Moon's perigee at J1980 + static let moonPerigeeLongitudeJ1980 = 349.383063 + /// Semi-major axis of Moon's orbit in km + static let moonOrbitSemiMajorAxis = 384401.0 + /// Eccentricity of the Moon's orbit + static let moonEccentricity = 0.054900 + /// Synodic month (new Moon to new Moon) + static let synodicMonth = 29.53058868 - // MARK: Solar methods - /// The mean anomaly for the sun + /// Get `MoonDetail`s for the given Julian day /// /// - Parameters: /// - julianDay: The date in Julian Days /// - /// - Returns: Mean anomaly for the sun, in radians - /// - /// Formula based https://aa.quae.nl/en/reken/hemelpositie.html#1_1 - /// https://github.com/microsoft/AirSim/blob/main/AirLib/include/common/EarthCelestial.hpp#L155 - /// and https://github.com/mourner/suncalc/blob/master/suncalc.js#L57 - static func solarMeanAnomaly(julianDay: Double) -> Double { - let daysSinceJ2000 = daysSinceJ2000(from: julianDay) - return AstronomicalConstant.degreesToRadians(357.5291 + 0.98560028 * daysSinceJ2000) + /// - Returns: MoonDetail object with moon details for the given Julian day + static func getMoonPhase(julianDay: Double) -> TinyMoon.MoonDetail { + calculateMoonData(for: julianDay) } - /// The ecliptic longitude λ [lambda] shows how far the celestial body is from the vernal equinox, measured along the ecliptic - /// - /// - Parameters: - /// - solarMeanAnomaly: in radians + /// Calculates the Moon's metadata for the given Julian day /// - /// - Returns: Ecliptic longitude, in radians - /// - /// Formula based on https://aa.quae.nl/en/reken/hemelpositie.html#1_1 - /// and https://github.com/microsoft/AirSim/blob/main/AirLib/include/common/EarthCelestial.hpp#L160 - /// and https://github.com/mourner/suncalc/blob/master/suncalc.js#L59 - static func eclipticLongitude(solarMeanAnomaly: Double) -> Double { - let center = - degreesToRadians( - 1.9148 * sin(solarMeanAnomaly) + 0.02 * sin(2 * solarMeanAnomaly) + 0.0003 * - sin(3 * solarMeanAnomaly)) // Equation of center - let perihelionInRadians = degreesToRadians(perihelion) - return solarMeanAnomaly + center + perihelionInRadians + Double.pi - } - - /// Get the position of the Sun on a given Julian Day - /// - /// - Parameters: + /// - Parameter: /// - julianDay: The date in Julian Days /// - /// - Returns: Tuple with δ declination (in radians) and α rightAscension (in radians) + /// - Returns: MoonData object with moon details for the given Julian day /// - /// Formula from https://aa.quae.nl/en/reken/hemelpositie.html#1 - /// https://github.com/microsoft/AirSim/blob/main/AirLib/include/common/EarthCelestial.hpp#L167 - /// and https://github.com/mourner/suncalc/blob/master/suncalc.js#L67 - static func sunCoordinates(julianDay: Double) -> (declination: Double, rightAscension: Double) { - let solarMeanAnomaly = solarMeanAnomaly(julianDay: julianDay) - let eclipticLongitude = eclipticLongitude(solarMeanAnomaly: solarMeanAnomaly) - - let declination = declination(longitude: eclipticLongitude, latitude: 0) - let rightAscension = rightAscension(longitude: eclipticLongitude, latitude: 0) - - return (declination, rightAscension) - } - - // MARK: Julian day methods - - /// The number of Julian days since 1 January 2000, 12:00 UTC - /// - /// `2451545.0` is the Julian date on 1 January 2000, 12:00 UTC, aka J2000 - static func daysSinceJ2000(from jd: Double) -> Double { - jd - 2451545.0 + /// Formula based on source code from https://www.fourmilab.ch/moontoolw/ + static func calculateMoonData(for julianDay: Double) -> TinyMoon.MoonDetail { + // Julian days since 1 January 1980, 00:00 UTC + let jdSinceJ1980 = julianDay - J1980 + // Mean anomaly of the Sun + let N = fixangle((360 / 365.2422) * jdSinceJ1980) + // Convert from perigee coordinates to J1980 + let M = fixangle(N + sunEclipticLongitudeJ1980 - sunPerigeeEclipticLongitudeJ1989) + // Solve for Kepler equation + var Ec = kepler(m: M, ecc: earthOrbitEccentricity) + Ec = sqrt((1.0 + earthOrbitEccentricity) / (1.0 - earthOrbitEccentricity)) * tan(Ec / 2.0) + // True anomaly + Ec = 2.0 * radiansToDegrees(atan(Ec)) + // Sun's geocentric ecliptic longitude + let lambdaSun = fixangle(Ec + sunPerigeeEclipticLongitudeJ1989) + + // Calculation of the Moon's position + // Moon's mean longitude + let moonMeanLongitude = fixangle(13.1763966 * jdSinceJ1980 + moonMeanLongitudeJ1980) + // Moon's mean anomaly + let moonMeanAnomaly = fixangle(moonMeanLongitude - 0.1114041 * jdSinceJ1980 - moonPerigeeLongitudeJ1980) + // Evection + let evection = 1.2739 * sin(degreesToRadians(2 * (moonMeanLongitude - lambdaSun) - moonMeanAnomaly)) + // Annual equation + let annualEquation = 0.1858 * sin(degreesToRadians(M)) + // Corrected term + let A3 = 0.37 * sin(degreesToRadians(M)) + // Corrected anomaly + let MmP = moonMeanAnomaly + evection - annualEquation - A3 + // Correction for the equation of center + let mEc = 6.2886 * sin(degreesToRadians(MmP)) + // Another correction term + let A4 = 0.214 * sin(degreesToRadians(2 * MmP)) + // Corrected longitude + let lP = moonMeanLongitude + evection + mEc - annualEquation + A4 + // Variation + let variation = 0.6583 * sin(degreesToRadians(2 * (lP - lambdaSun))) + // True longitude + let lPP = lP + variation + + // Calculation of the phase of the Moon + + // Age of the Moon in degrees + let moonAgeInDegrees = lPP - lambdaSun + // Age of the moon in days, minutes, hours + let (days, hour, minutes) = convertDegreesToDaysHoursMinutes(degrees: moonAgeInDegrees) + + // Phase of the Moon, where 0 = new and 100 = full + // AKA, illuminated fraction + let illuminatedFraction = (1 - cos(degreesToRadians(moonAgeInDegrees))) / 2 + + // Distance of moon from the center of the Earth + let moonDistance = (moonOrbitSemiMajorAxis * (1 - moonEccentricity * moonEccentricity)) / + (1 + moonEccentricity * cos(degreesToRadians(MmP + mEc))) + + // Moon age + // AKA days into the cycle + let moonAge = synodicMonth * (fixangle(moonAgeInDegrees) / 360.0) + + // Returns the terminator phase angle as a percentage of a full circle (i.e., 0 to 1) + let moonPhaseTerminator = fixangle(moonAgeInDegrees) / 360.0 + + return TinyMoon.MoonDetail( + julianDay: julianDay, + moonAgeinDegrees: moonAgeInDegrees, + ageOfMoon: (days, hour, minutes), + illuminatedFraction: illuminatedFraction, + moonDistance: moonDistance, + moonAge: moonAge, + phase: moonPhaseTerminator) } /// Calculates the Julian Day (JD) for a given Date @@ -211,193 +144,62 @@ extension TinyMoon { static func julianDay(_ date: Date) -> Double { (date.timeIntervalSince1970 * 1000) / (1000 * 60 * 60 * 24) - 0.5 + 2440588.0 } - } -} -extension TinyMoon.AstronomicalConstant { - // Julian date on 1 January 1980, 00:00 UTC - static let J1980 = 2444238.5 + static func degreesToRadians(_ degrees: Double) -> Double { + degrees * (Double.pi / 180) + } + + static func radiansToDegrees(_ radians: Double) -> Double { + radians * (180 / Double.pi) + } - // MARK: - Sun constants + // MARK: Private - // Ecliptic longitude of the Sun at J1980 - static let sunEclipticLongitudeJ1980 = 278.833540 - // Ecliptic longitude of the Sun's perigee at J1980 - static let sunPerigeeEclipticLongitudeJ1989 = 282.596403 - // Eccentricity of Earth's orbit - static let earthOrbitEccentricity = 0.016718 + private static func convertDegreesToDaysHoursMinutes(degrees: Double) -> (days: Int, hours: Int, minutes: Int) { + let degreesPerDay = 360.0 / synodicMonth + let totalDays = degrees / degreesPerDay - // MARK: - Moon constants + let days = Int(totalDays) + let fractionalDay = totalDays - Double(days) - // Moon's mean longitude at J1980 - static let moonMeanLongitudeJ1980 = 64.975464 - // Longitude of the Moon's perigee at J1980 - static let moonPerigeeLongitudeJ1980 = 349.383063 - // Mean longitude of the node at J1980 - static let moonNodeLongitude1980 = 151.950429 - // Inclination of the Moon's orbit - static let moonOrbitInclination = 5.145396 - // Semi-major axis of Moon's orbit in km - static let moonOrbitSemiMajorAxis = 384401.0 - // Eccentricity of the Moon's orbit - static let moonEccentricity = 0.054900 + let totalHours = fractionalDay * 24.0 + let hours = Int(totalHours) + let fractionalHour = totalHours - Double(hours) + let totalMinutes = fractionalHour * 60.0 + let minutes = Int(totalMinutes) - // MARK: - Mathematical formulas + return (days, hours, minutes) + } - static func fixangle(_ a: Double) -> Double { - return a - 360.0 * floor(a / 360.0) - } + // MARK: - Mathematical formulas - static func kepler(m: Double, ecc: Double) -> Double { + private static func fixangle(_ a: Double) -> Double { + a - 360.0 * floor(a / 360.0) + } + + private static func kepler(m: Double, ecc: Double) -> Double { var e = degreesToRadians(m) let mRad = degreesToRadians(m) var delta: Double - let maxIterations = 1000 // Set a limit for maximum iterations + let maxIterations = 1000 // Set a limit for maximum iterations var iteration = 0 - let epsilon = 1e-10 // Set a small threshold for convergence + let epsilon = 1e-10 // Set a small threshold for convergence repeat { - delta = e - ecc * sin(e) - mRad - e -= delta / (1.0 - ecc * cos(e)) - iteration += 1 - if iteration > maxIterations { - print("Warning: Kepler function did not converge") - break - } + delta = e - ecc * sin(e) - mRad + e -= delta / (1.0 - ecc * cos(e)) + iteration += 1 + if iteration > maxIterations { + print("Warning: Kepler function did not converge") + break + } } while abs(delta) > epsilon return e - } - - static func phase(julianDay: Double) -> Double { - // Calculation of the Sun's position - let jdSinceJ1980 = julianDay - J1980 - let N = fixangle((360 / 365.2422) * jdSinceJ1980) /* Mean anomaly of the Sun */ - let M = fixangle(N + sunEclipticLongitudeJ1980 - sunPerigeeEclipticLongitudeJ1989) /* Convert from perigee coordinates to epoch 1980.0 */ - var Ec = kepler(m: M, ecc: earthOrbitEccentricity) /* Solve equation of Kepler */ - Ec = sqrt((1.0 + earthOrbitEccentricity) / (1.0 - earthOrbitEccentricity)) * tan(Ec / 2.0) - Ec = 2.0 * radiansToDegrees(atan(Ec)) /* True anomaly */ - let Lambdasun = fixangle(Ec + sunPerigeeEclipticLongitudeJ1989) /* Sun's geocentric ecliptic longitude */ - - /* Calculation of the Moon's position */ - - /* Moon's mean longitude */ - let ml = fixangle(13.1763966 * jdSinceJ1980 + moonMeanLongitudeJ1980) - - /* Moon's mean anomaly */ - let MM = fixangle(ml - 0.1114041 * jdSinceJ1980 - moonPerigeeLongitudeJ1980) - - /* Evection */ - let Ev = 1.2739 * sin(degreesToRadians(2.0 * (ml - Lambdasun) - MM)) - - /* Annual equation */ - let Ae = 0.1858 * sin(degreesToRadians(M)) - - /* Correction term */ - let A3 = 0.37 * sin(degreesToRadians(M)) - - /* Corrected anomaly */ - let MmP = MM + Ev - Ae - A3 - - /* Correction for the equation of the centre */ - let mEc = 6.2886 * sin(degreesToRadians(MmP)) - - /* Another correction term */ - let A4 = 0.214 * sin(degreesToRadians(2.0 * MmP)) - - /* Corrected longitude */ - let lP = ml + Ev + mEc - Ae + A4 - - /* Variation */ - let V = 0.6583 * sin(degreesToRadians(2.0 * (lP - Lambdasun))) - - /* True longitude */ - let lPP = lP + V - - /* Calculation of the phase of the Moon */ - - /* Age of the Moon in degrees */ - let MoonAge = lPP - Lambdasun + } - /* Phase of the Moon */ - // let MoonPhase = (1.0 - cos(torad(MoonAge))) / 2.0 - // return MoonPhase - // return synmonth * (fixangle(MoonAge) / 360.0) - return fixangle(MoonAge) / 360.0 - } - - /// Calculates the Moon's phase, represented as a fraction - /// - /// - Parameter: - /// - julianDay: The date in Julian Days - /// - /// - Returns: Phase as a percentage of a full circle (i.e., 0 to 1), where 0.0` new moon, `0.25` first quarter, `0.5` full moon, `0.75` last quarter - /// - /// Formula based on source code from https://www.fourmilab.ch/moontoolw/ - static func _phase(julianDay: Double) -> Double { - // Julian days since 1 January 1980, 00:00 UTC - let jdSinceJ1980 = julianDay - J1980 - // Mean anomaly of the Sun - let N = fixangle((360 / 365.2422) * jdSinceJ1980) - // Convert from perigee coordinates to J1980 - let M = fixangle(N + sunEclipticLongitudeJ1980 - sunPerigeeEclipticLongitudeJ1989) - // Solve for Kepler equation - var Ec = kepler(m: M, ecc: earthOrbitEccentricity) - Ec = sqrt((1.0 + earthOrbitEccentricity) / (1.0 - earthOrbitEccentricity)) * tan(Ec / 2.0) - // True anomaly - Ec = 2.0 * radiansToDegrees(atan(Ec)) - // Sun's geocentric ecliptic longitude - let lambdaSun = fixangle(Ec + sunPerigeeEclipticLongitudeJ1989) - - // Calculation of the Moon's position - // Moon's mean longitude - let moonMeanLongitude = fixangle(13.1763966 * jdSinceJ1980 + moonMeanLongitudeJ1980) - // Moon's mean anomaly - let moonMeanAnomaly = fixangle(moonMeanLongitude - 0.1114041 * jdSinceJ1980 - moonPerigeeLongitudeJ1980) - // Moon's ascending node mean longitude - let moonAscendingNodeMeanLongitude = fixangle(moonNodeLongitude1980 - 0.0529539 * jdSinceJ1980) - // Evection - let evection = 1.2739 * sin(degreesToRadians(2 * (moonMeanLongitude - lambdaSun) - moonMeanAnomaly)) - // Annual equation - let annualEquation = 0.1858 * sin(degreesToRadians(M)) - // Corrected term - let A3 = 0.37 * sin(degreesToRadians(M)) - // Corrected anomaly - let MmP = moonMeanAnomaly + evection - annualEquation - A3 - // Correction for the equation of center - let mEc = 6.2886 * sin(degreesToRadians(MmP)) - // Another correction term - let A4 = 0.214 * sin(degreesToRadians(2 * MmP)) - // Corrected longitude - let lP = moonMeanLongitude + evection + mEc - annualEquation + A4 - // Variation - let variation = 0.6583 * sin(degreesToRadians(2 * (lP - lambdaSun))) - // True longitude - let lPP = lP + variation - // Corrected longitude of the node - let NP = moonAscendingNodeMeanLongitude - 0.16 * sin(degreesToRadians(M)) - // Y inclination coordinate - let y = sin(degreesToRadians(lPP - NP)) * cos(degreesToRadians(moonOrbitInclination)) - // X inclination coordinate - let x = cos(degreesToRadians(lPP - NP)) - // Ecliptic longitude - var lambdaMoon = radiansToDegrees(atan2(y, x)) - lambdaMoon += NP - // Ecliptic latitude - let betaM = radiansToDegrees(asin(sin(degreesToRadians(lPP - NP)) * sin(degreesToRadians(moonOrbitInclination)))) - - // Calculation of the phase of the Moon - // Age of the Moon in degrees - let moonAge = lPP - lambdaSun - // Phase of the Moon - let moonPhase = (1 - cos(degreesToRadians(moonAge))) / 2 - // Distance of moon from the center of the Earth - let moonDistance = (moonOrbitSemiMajorAxis * (1 - moonEccentricity * moonEccentricity)) / (1 + moonEccentricity * cos(degreesToRadians(MmP + mEc))) - - // Returns the terminator phase angle as a percentage of a full circle (i.e., 0 to 1) - let normalizedMoonPhase = fixangle(moonAge) / 360.0 - return normalizedMoonPhase } } + diff --git a/Sources/TinyMoon/TinyMoon+MoonDetail.swift b/Sources/TinyMoon/TinyMoon+MoonDetail.swift new file mode 100644 index 0000000..8f2ec14 --- /dev/null +++ b/Sources/TinyMoon/TinyMoon+MoonDetail.swift @@ -0,0 +1,25 @@ +// Created by manny_lopez on 7/12/24. + +import Foundation + +// MARK: - TinyMoon + MoonDetail + +extension TinyMoon { + + struct MoonDetail { + let julianDay: Double + /// Age of the Moon in degrees + let moonAgeinDegrees: Double + /// Age of the moon in days, minutes, hours + let ageOfMoon: (days: Int, hours: Int, minutes: Int) + /// Illuminated portion of the Moon, where 0 = new and 100 = full + let illuminatedFraction: Double + /// Distance of moon from the center of the Earth + let moonDistance: Double + /// Number of days elapsed into the synodic cycle + let moonAge: Double + /// Phase angle as a percentage of a full circle (i.e., 0 to 1). + /// Varies between `0.0` to `0.99`. `0.0` new moon, `0.25` first quarter, `0.5` full moon, `0.75` last quarter + let phase: Double + } +} diff --git a/Tests/TinyMoonTests/AstronomicalConstantTests.swift b/Tests/TinyMoonTests/AstronomicalConstantTests.swift index 939dfe0..6c83094 100644 --- a/Tests/TinyMoonTests/AstronomicalConstantTests.swift +++ b/Tests/TinyMoonTests/AstronomicalConstantTests.swift @@ -76,159 +76,38 @@ final class AstronomicalConstantTests: XCTestCase { XCTAssertEqual(julianDay, 2459814.4993055556) } - func test_astronomicalConstant_daysSinceJ2000() { - // 1 - var date = TinyMoon.formatDate(year: 2004, month: 01, day: 1) - var julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - XCTAssertEqual(julianDay, 2453005.5000) - - var daysSinceJ2000 = TinyMoon.AstronomicalConstant.daysSinceJ2000(from: julianDay) - XCTAssertEqual(daysSinceJ2000, 1460.5) - - // 2 - date = TinyMoon.formatDate(year: 2022, month: 08, day: 22, hour: 23, minute: 59) - julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - XCTAssertEqual(julianDay, 2459814.4993055556) - - daysSinceJ2000 = TinyMoon.AstronomicalConstant.daysSinceJ2000(from: julianDay) - XCTAssertEqual(daysSinceJ2000, 8269.499305555597) - } - - func test_astronomicalConstant_moonCoordinates() { - let date = TinyMoon.formatDate(year: 2004, month: 01, day: 1) - let julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - let moonCoordinates = TinyMoon.AstronomicalConstant.moonCoordinates(julianDay: julianDay) - - // Test values taken from https://aa.quae.nl/en/reken/hemelpositie.html#4 -// XCTAssertEqual(moonCoordinates.L, 339.683699626709) // 22.44 degrees -// XCTAssertEqual(moonCoordinates.M, 335.3891934066859) // 136.39 degrees -// XCTAssertEqual(moonCoordinates.F, 338.8510958397388) // 334.74 degrees -// -// XCTAssertEqual(moonCoordinates.longitude, 339.7594152822631) // 0.46740869456428196793 -// XCTAssertEqual(moonCoordinates.latitude, -0.038195520939872045) // -0.038195520939775448599 - - XCTAssertEqual(moonCoordinates.declination, 0.14456842408751425) - XCTAssertEqual(moonCoordinates.rightAscension, 0.4475918797699177) - XCTAssertEqual(moonCoordinates.distance, 400136.10760520655) - } - - func test_astronomicalConstant_declination() { - // Test values taken from https://aa.quae.nl/en/reken/hemelpositie.html#1_7 - let longitude = TinyMoon.AstronomicalConstant.degreesToRadians(168.737) - let latitude = TinyMoon.AstronomicalConstant.degreesToRadians(1.208) - let declination = TinyMoon.AstronomicalConstant.declination(longitude: longitude, latitude: latitude) - - XCTAssertEqual(declination, TinyMoon.AstronomicalConstant.degreesToRadians(5.567), accuracy: 1e-5) - - XCTAssertEqual(declination, 0.09717015472346271, accuracy: 1e-5) - } - - func test_astronomicalConstant_rightAscension() { - // Test values taken from https://aa.quae.nl/en/reken/hemelpositie.html#1_7 - let longitude = TinyMoon.AstronomicalConstant.degreesToRadians(168.737) - let latitude = TinyMoon.AstronomicalConstant.degreesToRadians(1.208) - let rightAscension = TinyMoon.AstronomicalConstant.rightAscension(longitude: longitude, latitude: latitude) - - XCTAssertEqual(rightAscension, TinyMoon.AstronomicalConstant.degreesToRadians(170.20), accuracy: 0.0015) - - XCTAssertEqual(rightAscension, 2.969160475404514) - } - - func test_astronomicalConstant_solarMeanAnomaly() { - var date = TinyMoon.formatDate(year: 2004, month: 01, day: 1) - var julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - var solarMeanAnomaly = TinyMoon.AstronomicalConstant.solarMeanAnomaly(julianDay: julianDay) - XCTAssertEqual(solarMeanAnomaly, 31.363537143773254) - - date = TinyMoon.formatDate(year: 2005, month: 02, day: 2) - julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - solarMeanAnomaly = TinyMoon.AstronomicalConstant.solarMeanAnomaly(julianDay: julianDay) - XCTAssertEqual(solarMeanAnomaly, 38.20992120161531) - - date = TinyMoon.formatDate(year: 2006, month: 03, day: 10) - julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - solarMeanAnomaly = TinyMoon.AstronomicalConstant.solarMeanAnomaly(julianDay: julianDay) - XCTAssertEqual(solarMeanAnomaly, 45.107911169441095) - - date = TinyMoon.formatDate(year: 2006, month: 03, day: 10, hour: 6) - julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - solarMeanAnomaly = TinyMoon.AstronomicalConstant.solarMeanAnomaly(julianDay: julianDay) - XCTAssertEqual(solarMeanAnomaly, 45.11221166193974) - - date = TinyMoon.formatDate(year: 2016, month: 04, day: 15, hour: 6) - julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - solarMeanAnomaly = TinyMoon.AstronomicalConstant.solarMeanAnomaly(julianDay: julianDay) - XCTAssertEqual(solarMeanAnomaly, 108.57027897193804) - - date = TinyMoon.formatDate(year: 2016, month: 04, day: 15, hour: 6, minute: 5) - julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - solarMeanAnomaly = TinyMoon.AstronomicalConstant.solarMeanAnomaly(julianDay: julianDay) - XCTAssertEqual(solarMeanAnomaly, 108.57033870099696) - - date = TinyMoon.formatDate(year: 2016, month: 04, day: 15, hour: 6, minute: 30) - julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - solarMeanAnomaly = TinyMoon.AstronomicalConstant.solarMeanAnomaly(julianDay: julianDay) - XCTAssertEqual(solarMeanAnomaly, 108.57063734631559) - - date = TinyMoon.formatDate(year: 2020, month: 10, day: 20, hour: 9, minute: 25) - julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - solarMeanAnomaly = TinyMoon.AstronomicalConstant.solarMeanAnomaly(julianDay: julianDay) - XCTAssertEqual(solarMeanAnomaly, 136.93877638455712) - } - - func test_astronomicalConstant_eclipticLongitude() { - let date = TinyMoon.formatDate(year: 2004, month: 01, day: 1) - let julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - let solarMeanAnomaly = TinyMoon.AstronomicalConstant.solarMeanAnomaly(julianDay: julianDay) - let eclipticLongitude = TinyMoon.AstronomicalConstant.eclipticLongitude(solarMeanAnomaly: solarMeanAnomaly) - XCTAssertEqual(eclipticLongitude, 36.299935502913485) - } - - func test_astronomicalConstant_sunCoordinates() { - let date = TinyMoon.formatDate(year: 2004, month: 01, day: 1) - let julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - let sunCoordinates = TinyMoon.AstronomicalConstant.sunCoordinates(julianDay: julianDay) - - XCTAssertEqual(sunCoordinates.declination, -0.4027393891133564) - XCTAssertEqual(sunCoordinates.rightAscension, -1.3840823935200117) - } - func test_astronomicalConstant_getMoonPhase() { // Full moon var date = TinyMoon.formatDate(year: 2024, month: 06, day: 22) var julianDay = TinyMoon.AstronomicalConstant.julianDay(date) var moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) - XCTAssertEqual(moonPhase.illuminatedFraction, 0.9978873506056865) + XCTAssertEqual(moonPhase.illuminatedFraction, 0.9999732292206713) XCTAssertEqual(moonPhase.phase, 0.49835304181785745) - XCTAssertEqual(moonPhase.angle, -2.8703533722710577) // New moon date = TinyMoon.formatDate(year: 2024, month: 07, day: 06, hour: 12, minute: 37) julianDay = TinyMoon.AstronomicalConstant.julianDay(date) moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) - XCTAssertEqual(moonPhase.illuminatedFraction, 0.007424715413253902) + XCTAssertEqual(moonPhase.illuminatedFraction, 0.0036280068150687517) XCTAssertEqual(moonPhase.phase, 0.019184351732275336) - XCTAssertEqual(moonPhase.angle, -1.9356676727903563) // First quarter date = TinyMoon.formatDate(year: 2024, month: 08, day: 12, hour: 15, minute: 18) julianDay = TinyMoon.AstronomicalConstant.julianDay(date) moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) - XCTAssertEqual(moonPhase.illuminatedFraction, 0.5105081080980992) + XCTAssertEqual(moonPhase.illuminatedFraction, 0.5017120238066795) XCTAssertEqual(moonPhase.phase, 0.2505449551679033) - XCTAssertEqual(moonPhase.angle, -1.2995618398922297) // Last quarter date = TinyMoon.formatDate(year: 2024, month: 08, day: 26, hour: 09, minute: 25) julianDay = TinyMoon.AstronomicalConstant.julianDay(date) moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) - XCTAssertEqual(moonPhase.illuminatedFraction, 0.5115383513011658) + XCTAssertEqual(moonPhase.illuminatedFraction, 0.49982435665155855) XCTAssertEqual(moonPhase.phase, 0.7500559090154013) - XCTAssertEqual(moonPhase.angle, 1.3632094278875226) } func test_phase() { From a8f1752a26252d3112c5ecbe34cf42ab44bf8efc Mon Sep 17 00:00:00 2001 From: mannylopez Date: Fri, 12 Jul 2024 15:37:52 -0700 Subject: [PATCH 4/9] Move code around --- .../TinyMoon+AstronomicalConstant.swift | 66 +++++++++---------- .../AstronomicalConstantTests.swift | 4 -- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift index 98bafe9..f7e6bad 100644 --- a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift +++ b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift @@ -43,18 +43,48 @@ extension TinyMoon { /// /// - Returns: MoonDetail object with moon details for the given Julian day static func getMoonPhase(julianDay: Double) -> TinyMoon.MoonDetail { - calculateMoonData(for: julianDay) + calculateMoonDetail(for: julianDay) } + /// Calculates the Julian Day (JD) for a given Date + /// + /// - Parameters: + /// - date: Any Swift Date to calculate the Julian Day for + /// + /// - Returns: The Julian Day number + /// + /// The Julian Day Count is a uniform count of days from a remote epoch in the past and is used for calculating the days between two events. + /// + /// The Julian day is calculated by combining the contributions from the years, months, and day, taking into account constant + /// + /// Formula based on https://github.com/mourner/suncalc/blob/master/suncalc.js#L29 + /// and https://github.com/microsoft/AirSim/blob/main/AirLib/include/common/EarthCelestial.hpp#L115 + /// - Note + /// - `2440588` is the Julian day for January 1, 1970, 12:00 UTC, aka J1970 + /// - `1000 * 60 * 60 * 24` is a day in milliseconds + static func julianDay(_ date: Date) -> Double { + (date.timeIntervalSince1970 * 1000) / (1000 * 60 * 60 * 24) - 0.5 + 2440588.0 + } + + static func degreesToRadians(_ degrees: Double) -> Double { + degrees * (Double.pi / 180) + } + + static func radiansToDegrees(_ radians: Double) -> Double { + radians * (180 / Double.pi) + } + + // MARK: Private + /// Calculates the Moon's metadata for the given Julian day /// /// - Parameter: /// - julianDay: The date in Julian Days /// - /// - Returns: MoonData object with moon details for the given Julian day + /// - Returns: MoonDetail object with moon details for the given Julian day /// /// Formula based on source code from https://www.fourmilab.ch/moontoolw/ - static func calculateMoonData(for julianDay: Double) -> TinyMoon.MoonDetail { + private static func calculateMoonDetail(for julianDay: Double) -> TinyMoon.MoonDetail { // Julian days since 1 January 1980, 00:00 UTC let jdSinceJ1980 = julianDay - J1980 // Mean anomaly of the Sun @@ -125,36 +155,6 @@ extension TinyMoon { phase: moonPhaseTerminator) } - /// Calculates the Julian Day (JD) for a given Date - /// - /// - Parameters: - /// - date: Any Swift Date to calculate the Julian Day for - /// - /// - Returns: The Julian Day number - /// - /// The Julian Day Count is a uniform count of days from a remote epoch in the past and is used for calculating the days between two events. - /// - /// The Julian day is calculated by combining the contributions from the years, months, and day, taking into account constant - /// - /// Formula based on https://github.com/mourner/suncalc/blob/master/suncalc.js#L29 - /// and https://github.com/microsoft/AirSim/blob/main/AirLib/include/common/EarthCelestial.hpp#L115 - /// - Note - /// - `2440588` is the Julian day for January 1, 1970, 12:00 UTC, aka J1970 - /// - `1000 * 60 * 60 * 24` is a day in milliseconds - static func julianDay(_ date: Date) -> Double { - (date.timeIntervalSince1970 * 1000) / (1000 * 60 * 60 * 24) - 0.5 + 2440588.0 - } - - static func degreesToRadians(_ degrees: Double) -> Double { - degrees * (Double.pi / 180) - } - - static func radiansToDegrees(_ radians: Double) -> Double { - radians * (180 / Double.pi) - } - - // MARK: Private - private static func convertDegreesToDaysHoursMinutes(degrees: Double) -> (days: Int, hours: Int, minutes: Int) { let degreesPerDay = 360.0 / synodicMonth let totalDays = degrees / degreesPerDay diff --git a/Tests/TinyMoonTests/AstronomicalConstantTests.swift b/Tests/TinyMoonTests/AstronomicalConstantTests.swift index 6c83094..df963c7 100644 --- a/Tests/TinyMoonTests/AstronomicalConstantTests.swift +++ b/Tests/TinyMoonTests/AstronomicalConstantTests.swift @@ -109,8 +109,4 @@ final class AstronomicalConstantTests: XCTestCase { XCTAssertEqual(moonPhase.illuminatedFraction, 0.49982435665155855) XCTAssertEqual(moonPhase.phase, 0.7500559090154013) } - - func test_phase() { - - } } From 6eb4cbf0e7039390dc69006c37a2b532ba40d351 Mon Sep 17 00:00:00 2001 From: mannylopez Date: Fri, 12 Jul 2024 17:01:53 -0700 Subject: [PATCH 5/9] Fix convertDegreesToDaysHoursMinutes by normalizing degrees within 360 --- .../TinyMoon+AstronomicalConstant.swift | 15 +++++++------ Sources/TinyMoon/TinyMoon+MoonDetail.swift | 21 +++++++++++-------- Sources/TinyMoon/TinyMoon.swift | 2 ++ .../AstronomicalConstantTests.swift | 15 +++++++++++++ Tests/TinyMoonTests/UTCTests.swift | 5 +++-- 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift index f7e6bad..91b5863 100644 --- a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift +++ b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift @@ -135,29 +135,28 @@ extension TinyMoon { let illuminatedFraction = (1 - cos(degreesToRadians(moonAgeInDegrees))) / 2 // Distance of moon from the center of the Earth - let moonDistance = (moonOrbitSemiMajorAxis * (1 - moonEccentricity * moonEccentricity)) / + let distanceFromCenterOfEarth = (moonOrbitSemiMajorAxis * (1 - moonEccentricity * moonEccentricity)) / (1 + moonEccentricity * cos(degreesToRadians(MmP + mEc))) - // Moon age - // AKA days into the cycle - let moonAge = synodicMonth * (fixangle(moonAgeInDegrees) / 360.0) + // Days into the synodic cycle + let daysElapsedInCycle = synodicMonth * (fixangle(moonAgeInDegrees) / 360.0) // Returns the terminator phase angle as a percentage of a full circle (i.e., 0 to 1) let moonPhaseTerminator = fixangle(moonAgeInDegrees) / 360.0 return TinyMoon.MoonDetail( julianDay: julianDay, - moonAgeinDegrees: moonAgeInDegrees, + daysElapsedInCycle: daysElapsedInCycle, ageOfMoon: (days, hour, minutes), illuminatedFraction: illuminatedFraction, - moonDistance: moonDistance, - moonAge: moonAge, + distanceFromCenterOfEarth: distanceFromCenterOfEarth, phase: moonPhaseTerminator) } private static func convertDegreesToDaysHoursMinutes(degrees: Double) -> (days: Int, hours: Int, minutes: Int) { + let normalizedDegrees = fixangle(degrees) let degreesPerDay = 360.0 / synodicMonth - let totalDays = degrees / degreesPerDay + let totalDays = normalizedDegrees / degreesPerDay let days = Int(totalDays) let fractionalDay = totalDays - Double(days) diff --git a/Sources/TinyMoon/TinyMoon+MoonDetail.swift b/Sources/TinyMoon/TinyMoon+MoonDetail.swift index 8f2ec14..c605267 100644 --- a/Sources/TinyMoon/TinyMoon+MoonDetail.swift +++ b/Sources/TinyMoon/TinyMoon+MoonDetail.swift @@ -8,18 +8,21 @@ extension TinyMoon { struct MoonDetail { let julianDay: Double - /// Age of the Moon in degrees - let moonAgeinDegrees: Double + /// Number of days elapsed into the synodic cycle, represented as a fraction + let daysElapsedInCycle: Double /// Age of the moon in days, minutes, hours let ageOfMoon: (days: Int, hours: Int, minutes: Int) - /// Illuminated portion of the Moon, where 0 = new and 100 = full + /// Illuminated portion of the Moon, where 0.0 = new and 0.99 = full let illuminatedFraction: Double - /// Distance of moon from the center of the Earth - let moonDistance: Double - /// Number of days elapsed into the synodic cycle - let moonAge: Double - /// Phase angle as a percentage of a full circle (i.e., 0 to 1). - /// Varies between `0.0` to `0.99`. `0.0` new moon, `0.25` first quarter, `0.5` full moon, `0.75` last quarter + /// Distance of moon from the center of the Earth, in kilometers + let distanceFromCenterOfEarth: Double + /// Phase of the Moon, represented as a fraction + /// + /// Varies between `0.0` to `0.99`. + /// `0.0` new moon, + /// `0.25` first quarter, + /// `0.5` full moon, + /// `0.75` last quarter let phase: Double } } diff --git a/Sources/TinyMoon/TinyMoon.swift b/Sources/TinyMoon/TinyMoon.swift index 473d55a..c73cbbf 100644 --- a/Sources/TinyMoon/TinyMoon.swift +++ b/Sources/TinyMoon/TinyMoon.swift @@ -57,6 +57,7 @@ public enum TinyMoon { day: Int, hour: Int = 00, minute: Int = 00, + second: Int = 00, timeZone: TimeZone = TimeZoneOption.createTimeZone(timeZone: .utc)) -> Date { @@ -66,6 +67,7 @@ public enum TinyMoon { components.day = day components.hour = hour components.minute = minute + components.second = second components.timeZone = timeZone return Calendar.current.date(from: components)! diff --git a/Tests/TinyMoonTests/AstronomicalConstantTests.swift b/Tests/TinyMoonTests/AstronomicalConstantTests.swift index df963c7..881fca3 100644 --- a/Tests/TinyMoonTests/AstronomicalConstantTests.swift +++ b/Tests/TinyMoonTests/AstronomicalConstantTests.swift @@ -109,4 +109,19 @@ final class AstronomicalConstantTests: XCTestCase { XCTAssertEqual(moonPhase.illuminatedFraction, 0.49982435665155855) XCTAssertEqual(moonPhase.phase, 0.7500559090154013) } + + func test_moontool() { + // Test taken from https://www.fourmilab.ch/moontoolw/ + let utcTimeZone = TinyMoon.TimeZoneOption.createTimeZone(timeZone: .utc) + let date = TinyMoon.formatDate(year: 1999, month: 07, day: 20, hour: 20, minute: 17, second: 40, timeZone: utcTimeZone) + let julianDay = TinyMoon.AstronomicalConstant.julianDay(date) + let moonDetail = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) + XCTAssertEqual(moonDetail.julianDay, 2451380.345601852) + XCTAssertEqual(moonDetail.ageOfMoon.days, 7) + XCTAssertEqual(moonDetail.ageOfMoon.hours, 19) + XCTAssertEqual(moonDetail.ageOfMoon.minutes, 30) + XCTAssertEqual(round(moonDetail.illuminatedFraction * 100), 55) + XCTAssertEqual(round(moonDetail.distanceFromCenterOfEarth), 402026) + + } } diff --git a/Tests/TinyMoonTests/UTCTests.swift b/Tests/TinyMoonTests/UTCTests.swift index 5e5f7c1..ee6b41d 100644 --- a/Tests/TinyMoonTests/UTCTests.swift +++ b/Tests/TinyMoonTests/UTCTests.swift @@ -21,7 +21,7 @@ final class UTCTests: XCTestCase { var incorrect = 0.0 let newMoonEmoji = TinyMoon.MoonPhase.newMoon.emoji - let waningCrescentEmoji = TinyMoon.MoonPhase.waningCrescent.emoji + let waxingCrescentEmoji = TinyMoon.MoonPhase.waxingCrescent.emoji // Returns a New Moon because it falls within this day's 24 hours var date = TinyMoon.formatDate(year: 2024, month: 09, day: 03, hour: 23, minute: 00) @@ -36,7 +36,8 @@ final class UTCTests: XCTestCase { let exactMoon = TinyMoon.calculateExactMoonPhase(date) XCTAssertNotEqual(exactMoon.exactMoonPhase, .newMoon) XCTAssertNotEqual(exactMoon.exactEmoji, newMoonEmoji) - if exactMoon.exactEmoji == waningCrescentEmoji { correct += 1 } else { incorrect += 1 } + XCTAssertEqual(exactMoon.exactMoonPhase.emoji, waxingCrescentEmoji) + if exactMoon.exactEmoji == waxingCrescentEmoji { correct += 1 } else { incorrect += 1 } print("Exact") printResults(.newMoon, correct: correct, incorrect: incorrect) From c37fca9f3244f3d9280418776db9ebbf49cb9211 Mon Sep 17 00:00:00 2001 From: mannylopez Date: Fri, 12 Jul 2024 17:09:08 -0700 Subject: [PATCH 6/9] Add code comments --- .../TinyMoon+AstronomicalConstant.swift | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift index 91b5863..9f2ce8b 100644 --- a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift +++ b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift @@ -173,10 +173,29 @@ extension TinyMoon { // MARK: - Mathematical formulas + /// Normalizes an angle to the range 0 to 360 degrees. + /// + /// This function ensures that any given angle is converted to its equivalent value within the range of 0 to 360 degrees. + /// + /// - Parameter a: The angle to be normalized, in degrees. + /// - Returns: The normalized angle, within the range [0, 360) degrees. + /// + /// Formula based on source code from https://www.fourmilab.ch/moontoolw/ private static func fixangle(_ a: Double) -> Double { a - 360.0 * floor(a / 360.0) } + /// Solves Kepler's equation for the eccentric anomaly. + /// + /// This function iteratively solves Kepler's equation to find the eccentric anomaly `e` for a given mean anomaly `m` and eccentricity `ecc`. + /// The solution is obtained using the Newton-Raphson method. + /// + /// - Parameters: + /// - m: The mean anomaly, in degrees. + /// - ecc: The eccentricity of the orbit. + /// - Returns: The eccentric anomaly, in radians. + /// + /// Formula based on source code from https://www.fourmilab.ch/moontoolw/ private static func kepler(m: Double, ecc: Double) -> Double { var e = degreesToRadians(m) let mRad = degreesToRadians(m) @@ -197,8 +216,6 @@ extension TinyMoon { return e } - - } } From bc958833b97e553243de95e8afe969e5163a1d1c Mon Sep 17 00:00:00 2001 From: mannylopez Date: Fri, 12 Jul 2024 17:12:35 -0700 Subject: [PATCH 7/9] Doccumentation for convertDegreesToDaysHoursMinutes --- Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift index 9f2ce8b..30ac733 100644 --- a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift +++ b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift @@ -153,6 +153,12 @@ extension TinyMoon { phase: moonPhaseTerminator) } + /// Converts an angle in degrees to a tuple of days, hours, and minutes. + /// + /// This function normalizes the given angle to the range [0, 360) degrees, then converts it into the equivalent number of days, hours, and minutes based on the synodic month. + /// + /// - Parameter degrees: The angle to be converted, in degrees. + /// - Returns: A tuple containing the equivalent days, hours, and minutes. private static func convertDegreesToDaysHoursMinutes(degrees: Double) -> (days: Int, hours: Int, minutes: Int) { let normalizedDegrees = fixangle(degrees) let degreesPerDay = 360.0 / synodicMonth From 6024550075f8b6bffab24ff27cb80ac11ece194a Mon Sep 17 00:00:00 2001 From: mannylopez Date: Fri, 12 Jul 2024 17:20:12 -0700 Subject: [PATCH 8/9] Fully test out MoonDetail --- .../AstronomicalConstantTests.swift | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/Tests/TinyMoonTests/AstronomicalConstantTests.swift b/Tests/TinyMoonTests/AstronomicalConstantTests.swift index 881fca3..07cdb04 100644 --- a/Tests/TinyMoonTests/AstronomicalConstantTests.swift +++ b/Tests/TinyMoonTests/AstronomicalConstantTests.swift @@ -76,38 +76,62 @@ final class AstronomicalConstantTests: XCTestCase { XCTAssertEqual(julianDay, 2459814.4993055556) } - func test_astronomicalConstant_getMoonPhase() { + func test_astronomicalConstant_getMoonPhase_moonDetail() { // Full moon var date = TinyMoon.formatDate(year: 2024, month: 06, day: 22) var julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - var moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) + var moonDetail = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) - XCTAssertEqual(moonPhase.illuminatedFraction, 0.9999732292206713) - XCTAssertEqual(moonPhase.phase, 0.49835304181785745) + XCTAssertEqual(moonDetail.julianDay, 2460483.5) + XCTAssertEqual(moonDetail.daysElapsedInCycle, 14.716658695349988) + XCTAssertEqual(moonDetail.ageOfMoon.days, 14) + XCTAssertEqual(moonDetail.ageOfMoon.hours, 17) + XCTAssertEqual(moonDetail.ageOfMoon.minutes, 11) + XCTAssertEqual(moonDetail.illuminatedFraction, 0.9999732292206713) + XCTAssertEqual(moonDetail.distanceFromCenterOfEarth, 382758.57898868265) + XCTAssertEqual(moonDetail.phase, 0.49835304181785745) // New moon date = TinyMoon.formatDate(year: 2024, month: 07, day: 06, hour: 12, minute: 37) julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) + moonDetail = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) - XCTAssertEqual(moonPhase.illuminatedFraction, 0.0036280068150687517) - XCTAssertEqual(moonPhase.phase, 0.019184351732275336) + XCTAssertEqual(moonDetail.julianDay, 2460498.0256944443) + XCTAssertEqual(moonDetail.daysElapsedInCycle, 0.5665252000982685) + XCTAssertEqual(moonDetail.ageOfMoon.days, 0) + XCTAssertEqual(moonDetail.ageOfMoon.hours, 13) + XCTAssertEqual(moonDetail.ageOfMoon.minutes, 35) + XCTAssertEqual(moonDetail.illuminatedFraction, 0.0036280068150687517) + XCTAssertEqual(moonDetail.distanceFromCenterOfEarth, 390943.47575863753) + XCTAssertEqual(moonDetail.phase, 0.019184351732275336) // First quarter date = TinyMoon.formatDate(year: 2024, month: 08, day: 12, hour: 15, minute: 18) julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) + moonDetail = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) - XCTAssertEqual(moonPhase.illuminatedFraction, 0.5017120238066795) - XCTAssertEqual(moonPhase.phase, 0.2505449551679033) + XCTAssertEqual(moonDetail.julianDay, 2460535.1375) + XCTAssertEqual(moonDetail.daysElapsedInCycle, 7.398740016912393) + XCTAssertEqual(moonDetail.ageOfMoon.days, 7) + XCTAssertEqual(moonDetail.ageOfMoon.hours, 9) + XCTAssertEqual(moonDetail.ageOfMoon.minutes, 34) + XCTAssertEqual(moonDetail.illuminatedFraction, 0.5017120238066795) + XCTAssertEqual(moonDetail.distanceFromCenterOfEarth, 398519.14701141417) + XCTAssertEqual(moonDetail.phase, 0.2505449551679033) // Last quarter date = TinyMoon.formatDate(year: 2024, month: 08, day: 26, hour: 09, minute: 25) julianDay = TinyMoon.AstronomicalConstant.julianDay(date) - moonPhase = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) - - XCTAssertEqual(moonPhase.illuminatedFraction, 0.49982435665155855) - XCTAssertEqual(moonPhase.phase, 0.7500559090154013) + moonDetail = TinyMoon.AstronomicalConstant.getMoonPhase(julianDay: julianDay) + + XCTAssertEqual(moonDetail.julianDay, 2460548.892361111) + XCTAssertEqual(moonDetail.daysElapsedInCycle, 22.14959253613732) + XCTAssertEqual(moonDetail.ageOfMoon.days, 22) + XCTAssertEqual(moonDetail.ageOfMoon.hours, 3) + XCTAssertEqual(moonDetail.ageOfMoon.minutes, 35) + XCTAssertEqual(moonDetail.illuminatedFraction, 0.49982435665155855) + XCTAssertEqual(moonDetail.distanceFromCenterOfEarth, 372205.09027872747) + XCTAssertEqual(moonDetail.phase, 0.7500559090154013) } func test_moontool() { From 3be8bbc818541d69bfc01810ae44f84157d611c2 Mon Sep 17 00:00:00 2001 From: mannylopez Date: Fri, 12 Jul 2024 17:26:54 -0700 Subject: [PATCH 9/9] Fully uncomment all UTC tests (all pass now)and improve exact moon tests --- Tests/TinyMoonTests/UTCTests.swift | 45 ++++++++++++++---------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/Tests/TinyMoonTests/UTCTests.swift b/Tests/TinyMoonTests/UTCTests.swift index ee6b41d..181e77c 100644 --- a/Tests/TinyMoonTests/UTCTests.swift +++ b/Tests/TinyMoonTests/UTCTests.swift @@ -24,7 +24,7 @@ final class UTCTests: XCTestCase { let waxingCrescentEmoji = TinyMoon.MoonPhase.waxingCrescent.emoji // Returns a New Moon because it falls within this day's 24 hours - var date = TinyMoon.formatDate(year: 2024, month: 09, day: 03, hour: 23, minute: 00) + let date = TinyMoon.formatDate(year: 2024, month: 09, day: 03, hour: 23, minute: 00) let moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .newMoon) XCTAssertEqual(moon.emoji, newMoonEmoji) @@ -32,11 +32,9 @@ final class UTCTests: XCTestCase { if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } // Even though it is the same day, at this exact time, it is not a New Moon - date = TinyMoon.formatDate(year: 2024, month: 09, day: 03, hour: 23, minute: 00) let exactMoon = TinyMoon.calculateExactMoonPhase(date) - XCTAssertNotEqual(exactMoon.exactMoonPhase, .newMoon) - XCTAssertNotEqual(exactMoon.exactEmoji, newMoonEmoji) - XCTAssertEqual(exactMoon.exactMoonPhase.emoji, waxingCrescentEmoji) + XCTAssertEqual(exactMoon.exactEmoji, waxingCrescentEmoji) + XCTAssertEqual(exactMoon.exactMoonPhase, .waxingCrescent) if exactMoon.exactEmoji == waxingCrescentEmoji { correct += 1 } else { incorrect += 1 } print("Exact") @@ -105,12 +103,12 @@ final class UTCTests: XCTestCase { XCTAssertEqual(moon.daysTillNewMoon, 0) if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } -// date = TinyMoon.formatDate(year: 2024, month: 09, day: 03) -// moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) -// XCTAssertEqual(moon.moonPhase, .newMoon) -// XCTAssertEqual(moon.emoji, newMoonEmoji) -// XCTAssertEqual(moon.daysTillNewMoon, 0) -// if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } + date = TinyMoon.formatDate(year: 2024, month: 09, day: 03) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) + XCTAssertEqual(moon.moonPhase, .newMoon) + XCTAssertEqual(moon.emoji, newMoonEmoji) + XCTAssertEqual(moon.daysTillNewMoon, 0) + if moon.emoji == newMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 10, day: 02) moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) @@ -236,14 +234,13 @@ final class UTCTests: XCTestCase { let waxingGibbousEmoji = TinyMoon.MoonPhase.waxingGibbous.emoji // At this exact time, the phase is Waxing Gibbous - var date = TinyMoon.formatDate(year: 2024, month: 08, day: 19, hour: 00, minute: 00) + let date = TinyMoon.formatDate(year: 2024, month: 08, day: 19, hour: 00, minute: 00) let exactMoon = TinyMoon.calculateExactMoonPhase(date) XCTAssertEqual(exactMoon.exactMoonPhase, .waxingGibbous) XCTAssertEqual(exactMoon.exactEmoji, waxingGibbousEmoji) if exactMoon.exactEmoji == waxingGibbousEmoji { correct += 1 } else { incorrect += 1 } // Although it is the same date and time, since a major phase (Full Moon) occurs within this day's 24 hours, this returns Full Moon - date = TinyMoon.formatDate(year: 2024, month: 08, day: 19, hour: 00, minute: 00) let moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) @@ -281,12 +278,12 @@ final class UTCTests: XCTestCase { XCTAssertEqual(moon.daysTillFullMoon, 0) if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } -// date = TinyMoon.formatDate(year: 2024, month: 04, day: 23) -// moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) -// XCTAssertEqual(moon.moonPhase, .fullMoon) -// XCTAssertEqual(moon.emoji, fullMoonEmoji) -// XCTAssertEqual(moon.daysTillFullMoon, 0) -// if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } + date = TinyMoon.formatDate(year: 2024, month: 04, day: 23) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) + XCTAssertEqual(moon.moonPhase, .fullMoon) + XCTAssertEqual(moon.emoji, fullMoonEmoji) + XCTAssertEqual(moon.daysTillFullMoon, 0) + if moon.emoji == fullMoonEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 05, day: 23) moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) @@ -373,11 +370,11 @@ final class UTCTests: XCTestCase { XCTAssertEqual(moon.emoji, lastQuarterEmoji) if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } -// date = TinyMoon.formatDate(year: 2024, month: 04, day: 02) -// moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) -// XCTAssertEqual(moon.moonPhase, .lastQuarter) -// XCTAssertEqual(moon.emoji, lastQuarterEmoji) -// if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } + date = TinyMoon.formatDate(year: 2024, month: 04, day: 02) + moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) + XCTAssertEqual(moon.moonPhase, .lastQuarter) + XCTAssertEqual(moon.emoji, lastQuarterEmoji) + if moon.emoji == lastQuarterEmoji { correct += 1 } else { incorrect += 1 } date = TinyMoon.formatDate(year: 2024, month: 05, day: 01) moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone)