diff --git a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift index 1afa99b..30ac733 100644 --- a/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift +++ b/Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift @@ -8,208 +8,220 @@ 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 - } + // MARK: - Moon constants + + /// 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 - 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. + /// Get `MoonDetail`s for the given Julian day /// /// - Parameters: - /// - longitude: in radians - /// - latitude: in radians - /// - /// - Returns: Declination, in radians + /// - julianDay: The date in Julian Days /// - /// 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)) + /// - Returns: MoonDetail object with moon details for the given Julian day + static func getMoonPhase(julianDay: Double) -> TinyMoon.MoonDetail { + calculateMoonDetail(for: julianDay) } - /// α The right ascension shows how far the body is from the vernal equinox, as measured along the celestial equator + /// Calculates the Julian Day (JD) for a given Date /// /// - Parameters: - /// - longitude: in radians - /// - latitude: in radians + /// - date: Any Swift Date to calculate the Julian Day for /// - /// - Returns: Right ascension, in radians + /// - Returns: The Julian Day number /// - /// 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 + /// 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. /// - /// - Parameters: - /// - julianDay: The date in Julian Days + /// The Julian day is calculated by combining the contributions from the years, months, and day, taking into account constant /// - /// - 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) + /// 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 } - /// Get Moon phase - /// - /// - 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 = 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Double.pi - - return (illuminatedFraction, phase, angle) + static func degreesToRadians(_ degrees: Double) -> Double { + degrees * (Double.pi / 180) } - // MARK: Solar methods + static func radiansToDegrees(_ radians: Double) -> Double { + radians * (180 / Double.pi) + } + + // MARK: Private - /// The mean anomaly for the sun + /// Calculates the Moon's metadata for the given Julian day /// - /// - Parameters: + /// - Parameter: /// - 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 + /// + /// Formula based on source code from https://www.fourmilab.ch/moontoolw/ + 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 + 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 distanceFromCenterOfEarth = (moonOrbitSemiMajorAxis * (1 - moonEccentricity * moonEccentricity)) / + (1 + moonEccentricity * cos(degreesToRadians(MmP + mEc))) + + // 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, + daysElapsedInCycle: daysElapsedInCycle, + ageOfMoon: (days, hour, minutes), + illuminatedFraction: illuminatedFraction, + distanceFromCenterOfEarth: distanceFromCenterOfEarth, + phase: moonPhaseTerminator) } - /// The ecliptic longitude λ [lambda] shows how far the celestial body is from the vernal equinox, measured along the ecliptic + /// Converts an angle in degrees to a tuple of days, hours, and minutes. /// - /// - Parameters: - /// - solarMeanAnomaly: in radians - /// - /// - 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 + /// 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. /// - /// - Parameters: - /// - julianDay: The date in Julian Days - /// - /// - Returns: Tuple with δ declination (in radians) and α rightAscension (in radians) - /// - /// 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) + /// - 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 + let totalDays = normalizedDegrees / degreesPerDay - let declination = declination(longitude: eclipticLongitude, latitude: 0) - let rightAscension = rightAscension(longitude: eclipticLongitude, latitude: 0) + let days = Int(totalDays) + let fractionalDay = totalDays - Double(days) - return (declination, rightAscension) - } + let totalHours = fractionalDay * 24.0 + let hours = Int(totalHours) + let fractionalHour = totalHours - Double(hours) - // MARK: Julian day methods + let totalMinutes = fractionalHour * 60.0 + let minutes = Int(totalMinutes) - /// 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 + return (days, hours, minutes) } - /// Calculates the Julian Day (JD) for a given Date + // MARK: - Mathematical formulas + + /// Normalizes an angle to the range 0 to 360 degrees. /// - /// - Parameters: - /// - date: Any Swift Date to calculate the Julian Day for + /// This function ensures that any given angle is converted to its equivalent value within the range of 0 to 360 degrees. /// - /// - Returns: The Julian Day number + /// - Parameter a: The angle to be normalized, in degrees. + /// - Returns: The normalized angle, within the range [0, 360) degrees. /// - /// 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. + /// 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. /// - /// The Julian day is calculated by combining the contributions from the years, months, and day, taking into account constant + /// 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. /// - /// 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 - /// - `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 + /// - 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) + 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 } } } + diff --git a/Sources/TinyMoon/TinyMoon+MoonDetail.swift b/Sources/TinyMoon/TinyMoon+MoonDetail.swift new file mode 100644 index 0000000..c605267 --- /dev/null +++ b/Sources/TinyMoon/TinyMoon+MoonDetail.swift @@ -0,0 +1,28 @@ +// Created by manny_lopez on 7/12/24. + +import Foundation + +// MARK: - TinyMoon + MoonDetail + +extension TinyMoon { + + struct MoonDetail { + let julianDay: 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.0 = new and 0.99 = full + let illuminatedFraction: Double + /// 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 94452f6..07cdb04 100644 --- a/Tests/TinyMoonTests/AstronomicalConstantTests.swift +++ b/Tests/TinyMoonTests/AstronomicalConstantTests.swift @@ -76,158 +76,76 @@ 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() { + 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.9978873506056865) - XCTAssertEqual(moonPhase.phase, 0.48536418607701615) - XCTAssertEqual(moonPhase.angle, -2.8703533722710577) + 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.007424715413253902) - XCTAssertEqual(moonPhase.phase, 0.02746179502131707) - XCTAssertEqual(moonPhase.angle, -1.9356676727903563) + 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.5105081080980992) - XCTAssertEqual(moonPhase.phase, 0.25334508096684466) - XCTAssertEqual(moonPhase.angle, -1.2995618398922297) + 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) + 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() { + // 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) - XCTAssertEqual(moonPhase.illuminatedFraction, 0.5115383513011658) - XCTAssertEqual(moonPhase.phase, 0.7463269026530461) - XCTAssertEqual(moonPhase.angle, 1.3632094278875226) } } 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..181e77c 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,25 +20,22 @@ 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 + 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: 02, hour: 00, minute: 00) - let moon = TinyMoon.calculateMoonPhase(date, timeZone: timeZone) + 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) 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) let exactMoon = TinyMoon.calculateExactMoonPhase(date) - XCTAssertNotEqual(exactMoon.exactMoonPhase, .newMoon) - XCTAssertNotEqual(exactMoon.exactEmoji, newMoonEmoji) - if exactMoon.exactEmoji == waningCrescentEmoji { correct += 1 } else { incorrect += 1 } + XCTAssertEqual(exactMoon.exactEmoji, waxingCrescentEmoji) + XCTAssertEqual(exactMoon.exactMoonPhase, .waxingCrescent) + if exactMoon.exactEmoji == waxingCrescentEmoji { correct += 1 } else { incorrect += 1 } print("Exact") printResults(.newMoon, correct: correct, incorrect: incorrect) @@ -46,96 +45,94 @@ 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) 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: 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 +147,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,21 +230,18 @@ 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 // 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: timeZone) + let moon = TinyMoon.calculateMoonPhase(date, timeZone: utcTimeZone) XCTAssertEqual(moon.moonPhase, .fullMoon) XCTAssertEqual(moon.emoji, fullMoonEmoji) XCTAssertEqual(moon.daysTillFullMoon, 0) @@ -263,89 +255,87 @@ 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) 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: 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,84 +350,82 @@ 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 } -// 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: 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 }