Skip to content

Commit

Permalink
Merge pull request #7 from mannylopez/is-new-moon
Browse files Browse the repository at this point in the history
Adds days till new moon
  • Loading branch information
mannylopez authored May 29, 2024
2 parents c7f5b3a + e6c4b10 commit 4f57a87
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 35 deletions.
67 changes: 50 additions & 17 deletions Sources/TinyMoon/TinyMoon.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
import Foundation

public struct Moon {
init(moonPhase: MoonPhase, lunarDay: Double, date: Date) {
public struct Moon: Hashable {
init(moonPhase: MoonPhase, lunarDay: Double, maxLunarDay: Double, date: Date) {
self.moonPhase = moonPhase
self.name = moonPhase.rawValue
self.emoji = moonPhase.emoji
self.lunarDay = lunarDay
self.maxLunarDay = maxLunarDay
self.date = date
}

public let moonPhase: MoonPhase
public let name: String
public let emoji: String
public let lunarDay: Double
public let maxLunarDay: Double
public let date: Date
private var wholeLunarDay: Int {
Int(floor(lunarDay))
}

// Returns `0` if the current `date` is a full moon
public var daysTillFullMoon: Int {
let wholeLunarDay = lround(lunarDay)
if wholeLunarDay > 15 {
return 29 - wholeLunarDay + 15
if wholeLunarDay > 14 {
return 29 - wholeLunarDay + 14
} else {
return 14 - wholeLunarDay
}
}

// Returns `0` if the current `date` is a new moon
public var daysTillNewMoon: Int {
if wholeLunarDay == 0 {
return 0
} else {
return 15 - wholeLunarDay
let daysTillNextNewMoon = Int(ceil(maxLunarDay)) - wholeLunarDay
return daysTillNextNewMoon
}
}

Expand Down Expand Up @@ -96,6 +111,14 @@ public enum MoonPhase: String {
public struct TinyMoon {
public init() { }
public func calculateMoonPhase(_ date: Date = Date()) -> Moon {
let lunarDay = lunarDay(for: date)
let maxLunarDay = maxLunarDayInCycle(starting: date)
let moonPhase = moonPhase(lunarDay: Int(floor(lunarDay)))
let moon = Moon(moonPhase: moonPhase, lunarDay: lunarDay, maxLunarDay: maxLunarDay, date: date)
return moon
}

internal func lunarDay(for date: Date) -> Double {
let synodicMonth = 29.53058770576
let calendar = Calendar.current
let components = calendar.dateComponents([.day, .month, .year], from: date)
Expand All @@ -112,28 +135,38 @@ public struct TinyMoon {
let dateDifference = julianDay(year: year, month: month, day: day) - julianDay(year: 2000, month: 1, day: 6)
// Divide by synodic month `29.53058770576`
let lunarDay = (dateDifference / synodicMonth).truncatingRemainder(dividingBy: 1) * synodicMonth
return lunarDay
}

let moonPhase = moonPhase(lunarDay: lround(lunarDay))
let moon = Moon(moonPhase: moonPhase, lunarDay: lunarDay, date: date)
return moon
internal func maxLunarDayInCycle(starting date: Date) -> Double {
let maxLunarDay = lunarDay(for: date)
let calendar = Calendar.current
if let tomorrow = calendar.date(byAdding: .day, value: 1, to: date) {
if lunarDay(for: tomorrow) < maxLunarDay {
return maxLunarDay
} else {
return maxLunarDayInCycle(starting: tomorrow)
}
}
return maxLunarDay
}

private func moonPhase(lunarDay: Int) -> MoonPhase {
internal func moonPhase(lunarDay: Int) -> MoonPhase {
if lunarDay < 1 {
return .newMoon
} else if lunarDay < 7 {
} else if lunarDay < 6 {
return .waxingCrescent
} else if lunarDay < 8 {
} else if lunarDay < 7 {
return .firstQuarter
} else if lunarDay < 15 {
} else if lunarDay < 14 {
return .waxingGibbous
} else if lunarDay < 16 {
} else if lunarDay < 15 {
return .fullMoon
} else if lunarDay < 22 {
} else if lunarDay < 21 {
return .waningGibbous
} else if lunarDay < 23 {
} else if lunarDay < 22 {
return .lastQuarter
} else if lunarDay < 29 {
} else if lunarDay < 30 {
return .waningCrescent
} else {
return .newMoon
Expand Down
82 changes: 82 additions & 0 deletions Tests/TinyMoonTests/TestHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Created by manny_lopez on 5/24/24.

import Foundation
@testable import TinyMoon

struct TestHelper {
let tinyMoon = TinyMoon()
static var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd HH:mm"
return formatter
}

static func formatDate(year: Int, month: Int, day: Int) -> Date {
guard let date = TestHelper.dateFormatter.date(from: "\(year)/\(month)/\(day) 00:00") else {
fatalError("Invalid date")
}
return date
}

/// Helper function to return a moon object for a given Date
func moonDay(year: Int, month: Int, day: Int) -> Moon {
let fullMoonDate = TestHelper.dateFormatter.date(from: "\(year)/\(month)/\(day) 00:00")
let moon = tinyMoon.calculateMoonPhase(fullMoonDate!)
return moon
}

/// Helper function to return an array of moon objects for a given range of Dates
func moonRange(year: Int, month: Int, days: ClosedRange<Int>) -> [Moon] {
var moons: [Moon] = []

moons = days.map({ day in
moonDay(year: year, month: month, day: day)
})

return moons
}

/// Helper function to return a full month's moon objects
func moonMonth(month: Helper.Month) -> [Moon] {
var moons: [Moon] = []

Helper.months2024[month]?.forEach({ day in
moons.append(moonDay(year: 2024, month: month.rawValue, day: day))
})

return moons
}

}

enum Helper {
enum Month: Int {
case january = 1
case february
case march
case april
case may
case june
case july
case august
case september
case october
case november
case december
}

static let months2024: [Month: ClosedRange<Int>] = [
.january: 1...31,
.february: 1...29, // Leap year in 2024
.march: 1...31,
.april: 1...30,
.may: 1...31,
.june: 1...30,
.july: 1...31,
.august: 1...30,
.september: 1...30,
.october: 1...31,
.november: 1...30,
.december: 1...31,
]
}
82 changes: 64 additions & 18 deletions Tests/TinyMoonTests/TinyMoonTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,72 @@ import XCTest

final class TinyMoonTests: XCTestCase {
func test_tinyMoon_calculateMoonPhase_returnsFullMoon() throws {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd HH:mm"
let fullMoonDate = formatter.date(from: "2024/04/23 00:00")
let tinyMoon = TinyMoon()
let moonPhase = tinyMoon.calculateMoonPhase(fullMoonDate!)
XCTAssertTrue(moonPhase.isFullMoon())
XCTAssertEqual(moonPhase.fullMoonName, "Pink Moon")
XCTAssertEqual(moonPhase.daysTillFullMoon, 0)
XCTAssertEqual(moonPhase.emoji, "\u{1F315}") // 🌕
let date = TestHelper.formatDate(year: 2024, month: 04, day: 23)
let moon = TestHelper().tinyMoon.calculateMoonPhase(date)

XCTAssertTrue(moon.isFullMoon())
XCTAssertEqual(moon.fullMoonName, "Pink Moon")
XCTAssertEqual(moon.daysTillFullMoon, 0)
XCTAssertEqual(moon.emoji, "\u{1F315}") // 🌕
}

func test_moon_daysTillFullMoon_returnsCorrectDays() throws {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd HH:mm"
let fullMoonDate = formatter.date(from: "2024/04/22 00:00")
let tinyMoon = TinyMoon()
let moonPhase = tinyMoon.calculateMoonPhase(fullMoonDate!)
XCTAssertFalse(moonPhase.isFullMoon())
XCTAssertNil(moonPhase.fullMoonName)
XCTAssertEqual(moonPhase.daysTillFullMoon, 1)
XCTAssertEqual(moonPhase.emoji, "\u{1F314}") // 🌔
let date = TestHelper.formatDate(year: 2024, month: 04, day: 22)
let moon = TestHelper().tinyMoon.calculateMoonPhase(date)

XCTAssertFalse(moon.isFullMoon())
XCTAssertNil(moon.fullMoonName)
XCTAssertEqual(moon.daysTillFullMoon, 1)
XCTAssertEqual(moon.emoji, "\u{1F314}") // 🌔
}

func test_tinyMoon_calculateMoonPhase_returnsNewMoon() throws {
var date = TestHelper.formatDate(year: 2024, month: 11, day: 01)
let testHelper = TestHelper()
var moon = testHelper.tinyMoon.calculateMoonPhase(date)
XCTAssertEqual(moon.emoji, "\u{1F311}") // 🌑
XCTAssertEqual(moon.daysTillNewMoon, 0)

date = TestHelper.formatDate(year: 2024, month: 12, day: 1)
moon = testHelper.tinyMoon.calculateMoonPhase(date)
XCTAssertEqual(moon.emoji, "\u{1F311}") // 🌑
XCTAssertEqual(moon.daysTillNewMoon, 0)
}

func test_moon_uniquePhases() {
let testHelper = TestHelper()
var months: [Helper.Month] = [.january, .february, .april, .may, .june, .july, .august, .september, .october, .november]

months.forEach { month in
let moons = testHelper.moonMonth(month: month)
let emojis = moons.compactMap { moon in
switch moon.moonPhase {
case .newMoon, .firstQuarter, .fullMoon, .lastQuarter:
return moon.emoji
default:
break
}
return nil
}

XCTAssertEqual(emojis.count, 4)
}

months = [.march, .december]
months.forEach { month in
let moons = testHelper.moonMonth(month: month)
let emojis = moons.compactMap { moon in
switch moon.moonPhase {
case .newMoon, .firstQuarter, .fullMoon, .lastQuarter:
return moon.emoji
default:
break
}
return nil
}

XCTAssertEqual(emojis.count, 5)
}
}
}

0 comments on commit 4f57a87

Please sign in to comment.