Skip to content

Commit 39a9eee

Browse files
authored
Merge pull request #26 from mannylopez/ml--timezone-support
Add timezone support
2 parents 46c596e + 2954f68 commit 39a9eee

File tree

6 files changed

+479
-227
lines changed

6 files changed

+479
-227
lines changed

Sources/TinyMoon/TinyMoon+AstronomicalConstant.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,7 @@ extension TinyMoon {
259259
/// `1440` is the number of minutes in a day, and `86400` is the number of seconds in a day
260260
let dayFraction = (Double(hour) - 12) / 24 + Double(minute) / 1440 + Double(second) / 86400
261261
let julianDayWithTime = jdn + dayFraction
262-
let roundedJulianDay = (julianDayWithTime * 10000).rounded() / 10000
263-
264-
return roundedJulianDay
262+
return (julianDayWithTime * 10000).rounded() / 10000
265263
}
266264
}
267265
}

Sources/TinyMoon/TinyMoon+Moon.swift

Lines changed: 81 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,19 @@ extension TinyMoon {
1919

2020
// MARK: Lifecycle
2121

22-
init(date: Date) {
22+
init(date: Date, timeZone: TimeZone = TimeZone.current) {
2323
self.date = date
2424
let julianDay = AstronomicalConstant.julianDay(date)
2525
let moonPhaseData = AstronomicalConstant.getMoonPhase(julianDay: julianDay)
2626
phaseFraction = moonPhaseData.phase
2727
illuminatedFraction = moonPhaseData.illuminatedFraction
28-
moonPhase = Moon.moonPhase(julianDay: julianDay, phaseFraction: phaseFraction)
28+
29+
moonPhase = Moon.moonPhase(phaseFraction: phaseFraction, date: date, timeZone: timeZone)
30+
2931
name = moonPhase.rawValue
3032
emoji = moonPhase.emoji
31-
daysTillFullMoon = Moon.daysUntilFullMoon(moonPhase: moonPhase, julianDay: julianDay)
32-
daysTillNewMoon = Moon.daysUntilNewMoon(moonPhase: moonPhase, julianDay: julianDay)
33+
daysTillFullMoon = Moon.daysUntilFullMoon(moonPhase: moonPhase, date: date, timeZone: timeZone)
34+
daysTillNewMoon = Moon.daysUntilNewMoon(moonPhase: moonPhase, date: date, timeZone: timeZone)
3335
}
3436

3537
// MARK: Public
@@ -71,116 +73,148 @@ extension TinyMoon {
7173

7274
// MARK: Internal
7375

74-
static func daysUntilFullMoon(moonPhase: MoonPhase, julianDay: Double) -> Int {
76+
static func daysUntilFullMoon(
77+
moonPhase: MoonPhase,
78+
date: Date,
79+
timeZone: TimeZone = TimeZone.current)
80+
-> Int
81+
{
7582
if moonPhase == .fullMoon {
7683
return 0
7784
}
7885
var phase: MoonPhase = moonPhase
79-
var currentJulianDay = julianDay
86+
var currentDate = date
8087
var daysUntilFullMoon = 0
88+
var calendar = Calendar.current
89+
calendar.timeZone = timeZone
8190

8291
while phase != .fullMoon {
83-
if let majorMoonPhase = dayIncludesMajorMoonPhase(julianDay: currentJulianDay) {
92+
if let majorMoonPhase = dayIncludesMajorMoonPhase(date: currentDate, timeZone: timeZone) {
8493
phase = majorMoonPhase
85-
currentJulianDay += 1
8694
daysUntilFullMoon += 1
95+
currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate)!
8796
} else {
88-
currentJulianDay += 1
8997
daysUntilFullMoon += 1
98+
currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate)!
9099
}
91100
}
92101

93102
return daysUntilFullMoon - 1
94103
}
95104

96-
static func daysUntilNewMoon(moonPhase: MoonPhase, julianDay: Double) -> Int {
105+
static func daysUntilNewMoon(
106+
moonPhase: MoonPhase,
107+
date: Date,
108+
timeZone: TimeZone = TimeZone.current)
109+
-> Int
110+
{
97111
if moonPhase == .newMoon {
98112
return 0
99113
}
100114
var phase: MoonPhase = moonPhase
101-
var currentJulianDay = julianDay
115+
var currentDate = date
102116
var daysUntilNewMoon = 0
117+
var calendar = Calendar.current
118+
calendar.timeZone = timeZone
103119

104120
while phase != .newMoon {
105-
if let majorMoonPhase = dayIncludesMajorMoonPhase(julianDay: currentJulianDay) {
121+
if let majorMoonPhase = dayIncludesMajorMoonPhase(date: currentDate, timeZone: timeZone) {
106122
phase = majorMoonPhase
107-
currentJulianDay += 1
108123
daysUntilNewMoon += 1
124+
currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate)!
109125
} else {
110-
currentJulianDay += 1
111126
daysUntilNewMoon += 1
127+
currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate)!
112128
}
113129
}
114130

115131
return daysUntilNewMoon - 1
116132
}
117133

118134

119-
/// Determines the moon phase for a given Julian day, considering both major and minor moon phases.
135+
/// Determines the moon phase for a given date, considering both major and minor moon phases.
120136
///
121-
/// This function first checks if the specified Julian day includes one of the major moon phases (new moon, first quarter, full moon, last quarter).
137+
/// This function first checks if the specified date includes one of the major moon phases (new moon, first quarter, full moon, last quarter) within it's 24 hours.
122138
/// If a major moon phase occurs within the day, that phase is returned. Otherwise, the function calculates the minor moon phase based on the given phase fraction.
123139
///
124140
/// - Parameters:
125-
/// - julianDay: A `Double` representing the Julian day for which to determine the moon phase.
126141
/// - phaseFraction: A `Double` representing the fractional part of the moon's phase, used to calculate the minor moon phase if no major moon phase occurs on the given day.
142+
/// - date: The day for which to determine the moon phase.
143+
/// - timeZone: The timezone to use when calculating the date. Defaults to the system's current timezone.
127144
///
128145
/// - Returns: A `MoonPhase` indicating either the major moon phase occurring on the specified day or the minor moon phase calculated from the phase fraction.
129146
///
130147
/// Example Usage:
131148
///
132149
/// ```swift
133-
/// let julianDay = 2451545.0 // An example Julian day
134150
/// let phaseFraction = 0.1 // An example phase fraction
135-
/// let moonPhase = moonPhase(julianDay: julianDay, phaseFraction: phaseFraction)
151+
/// let utcTimeZone = TimeZone(identifier: "UTC")!
152+
/// let moonPhase = moonPhase(phaseFraction: phaseFraction, date: Date(), timeZone: utcTimeZone)
136153
/// print(moonPhase) // Output depends on the calculated or determined moon phase
137154
/// ```
138-
static func moonPhase(julianDay: Double, phaseFraction: Double) -> MoonPhase {
139-
if let moonPhase = Moon.dayIncludesMajorMoonPhase(julianDay: julianDay) {
155+
static func moonPhase(
156+
phaseFraction: Double,
157+
date: Date,
158+
timeZone: TimeZone = TimeZone.current)
159+
-> MoonPhase
160+
{
161+
if let moonPhase = Moon.dayIncludesMajorMoonPhase(date: date, timeZone: timeZone) {
140162
moonPhase
141163
} else {
142164
Moon.minorMoonPhase(phaseFraction: phaseFraction)
143165
}
144166
}
145167

146-
/// Checks if a given Julian day includes one of the major moon phases.
168+
/// Checks if a given date includes one of the major moon phases.
147169
///
148-
/// This function evaluates whether the specified Julian day encompasses any of the major moon phases: new moon, first quarter, full moon, or last quarter.
149-
/// It does so by calculating the moon phase fraction at the start and end of the Julian day and checking if the exact point of a major moon phase falls within this range.
170+
/// This function evaluates whether the specified date encompasses any of the major moon phases: new moon, first quarter, full moon, or last quarter.
171+
/// It does so by calculating the moon phase fraction at the start and end of the day and checking if the exact point of a major moon phase falls within this range.
150172
///
151-
/// - Parameter julianDay: A `Double` representing the Julian day to check for major moon phases.
173+
/// - Parameters:
174+
/// - date: Day to check for major moon phases.
175+
/// - timeZone: The timezone to use when calculating the date. Defaults to the system's current timezone.
152176
///
153177
/// - Returns: An optional `MoonPhase` representing the major moon phase occurring on the specified day, if any. Returns `nil` if no major moon phase occurs on that day.
154178
///
155179
/// - Note: The determination of major moon phases is based on predefined thresholds for phase fractions that correspond to the significant points in a lunar cycle
156-
static func dayIncludesMajorMoonPhase(julianDay: Double) -> MoonPhase? {
157-
let startAndEndOfJulianDay = startAndEndOfJulianDay(julianDay: julianDay)
158-
let moonPhaseFractionAtStart = AstronomicalConstant.getMoonPhase(julianDay: startAndEndOfJulianDay.start).phase
159-
let moonPhaseFractionAtEnd = AstronomicalConstant.getMoonPhase(julianDay: startAndEndOfJulianDay.end).phase
180+
static func dayIncludesMajorMoonPhase(
181+
date: Date,
182+
timeZone: TimeZone = TimeZone.current)
183+
-> MoonPhase?
184+
{
185+
let (startJulianDay, endJulianDay) = julianStartAndEndOfDay(date: date, timeZone: timeZone)
186+
let moonPhaseFractionAtStart = AstronomicalConstant.getMoonPhase(julianDay: startJulianDay).phase
187+
let moonPhaseFractionAtEnd = AstronomicalConstant.getMoonPhase(julianDay: endJulianDay).phase
160188
return majorMoonPhaseInRange(start: moonPhaseFractionAtStart, end: moonPhaseFractionAtEnd)
161189
}
162190

163-
/// Calculates Julian days for a 00:00 UT and 23:00 UT centered around the given Julian day.
191+
/// Calculates Julian day values for the beginning of the day, 00:00, and 24 hours from there, representing a day's full Julian value range.
164192
///
165-
/// - Parameter julianDay: The Julian day around which to calculate the 24-hour period. The fractional part of this parameter determines the starting point of the period.
193+
/// - Parameters:
194+
/// - date: The date around which to calculate the 24-hour period.
195+
/// - timeZone: The timezone to use when calculating the date. Defaults to the system's current timezone.
166196
///
167-
/// - Returns: An array of two Julian days representing the start of the 24-hour period at 00:00 UT and the end point at 23:59 UT.
197+
/// - Returns: A tuple of two Julian days representing the start of the 24-hour period at 00:00 and the end point at 00:00 the next day.
168198
///
169199
/// Example Usage:
170200
///
171201
/// ```swift
172-
/// let jd = 2460320.5 // Represents January 11, 2024 at 00:00 UT
173-
/// let julianDays = startAndEndOfJulianDay(julianDay: jd)
174-
/// print(julianDays)
175-
/// // Output: [2460320.5, 2460321.4993]
176-
/// // Where:
177-
/// // 2460320.5 = January 11, 2024 at 00:00 UT
178-
/// // 2460321.25 = January 11, 2024 at 23:59 UT
202+
/// let date = Date()
203+
/// let utcTimeZone = TimeZone(identifier: "UTC")!
204+
/// let (start, end) = julianStartAndEndOfDay(date: date, timeZone: utcTimeZone)
205+
/// print(start)
206+
/// // 2460586.5 = October 3, 2024 at 00:00 UTC
207+
/// print(end)
208+
/// // 2460587.5 = October 4, 2024 at 00:00 UTC
179209
/// ```
180-
static func startAndEndOfJulianDay(julianDay: Double) -> (start: Double, end: Double) {
181-
let base = floor(julianDay) + (julianDay.truncatingRemainder(dividingBy: 1) < 0.5 ? -1 : 0)
182-
let arr = [0.5, 1.4993].map { base + $0 }
183-
return (start: arr[0], end: arr[1])
210+
static func julianStartAndEndOfDay(date: Date, timeZone: TimeZone = TimeZone.current) -> (start: Double, end: Double) {
211+
var calendar = Calendar.current
212+
calendar.timeZone = timeZone
213+
let startOfDay = calendar.startOfDay(for: date)
214+
let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)
215+
let startJulianDay = AstronomicalConstant.julianDay(startOfDay)
216+
let endJulianDay = AstronomicalConstant.julianDay(endOfDay!)
217+
return (start: startJulianDay, end: endJulianDay)
184218
}
185219

186220
/// Determines if the range between two moon phase fractions includes one of the major moon phases.
@@ -232,16 +266,14 @@ extension TinyMoon {
232266
}
233267

234268
static func minorMoonPhase(phaseFraction: Double) -> MoonPhase {
235-
if phaseFraction < 0.23 {
269+
if phaseFraction < 0.25 {
236270
.waxingCrescent
237-
} else if phaseFraction < 0.48 {
271+
} else if phaseFraction < 0.50 {
238272
.waxingGibbous
239-
} else if phaseFraction < 0.73 {
273+
} else if phaseFraction < 0.75 {
240274
.waningGibbous
241-
} else if phaseFraction < 0.98 {
242-
.waningCrescent
243275
} else {
244-
.newMoon
276+
.waningCrescent
245277
}
246278
}
247279

Sources/TinyMoon/TinyMoon.swift

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ public enum TinyMoon {
1313
/// If no major moon phase occurs within the date's 24 hours (starting at 00:00 and ending at 23:59), the object will represent the moon phase closest to the specified date and time.
1414
///
1515
/// - Note: Unlike `ExactMoon`, this object will prioritize major moon phases occurring at any point within a 24-hour period.
16-
public static func calculateMoonPhase(_ date: Date = Date()) -> Moon {
17-
Moon(date: date)
16+
public static func calculateMoonPhase(_ date: Date = Date(), timeZone: TimeZone = TimeZone.current) -> Moon {
17+
Moon(date: date, timeZone: timeZone)
1818
}
1919

2020
/// The `ExactMoon` object for a specific date and time.
@@ -33,27 +33,42 @@ public enum TinyMoon {
3333

3434
// MARK: Internal
3535

36+
enum TimeZoneOption {
37+
case utc
38+
case pacific
39+
case tokyo
40+
41+
static func createTimeZone(timeZone: TimeZoneOption) -> TimeZone {
42+
switch timeZone {
43+
case .utc:
44+
TimeZone(identifier: "UTC")!
45+
case .pacific:
46+
TimeZone(identifier: "America/Los_Angeles")!
47+
case .tokyo:
48+
TimeZone(identifier: "Asia/Tokyo")!
49+
}
50+
}
51+
}
52+
53+
/// Creates a Date from the given arguments. Default is in UTC timezone.
3654
static func formatDate(
3755
year: Int,
3856
month: Int,
3957
day: Int,
4058
hour: Int = 00,
41-
minute: Int = 00)
59+
minute: Int = 00,
60+
timeZone: TimeZone = TimeZoneOption.createTimeZone(timeZone: .utc))
4261
-> Date
4362
{
44-
guard let date = TinyMoon.dateFormatter.date(from: "\(year)/\(month)/\(day) \(hour):\(minute)") else {
45-
fatalError("Invalid date")
46-
}
47-
return date
48-
}
49-
50-
// MARK: Private
63+
var components = DateComponents()
64+
components.year = year
65+
components.month = month
66+
components.day = day
67+
components.hour = hour
68+
components.minute = minute
69+
components.timeZone = timeZone
5170

52-
private static var dateFormatter: DateFormatter {
53-
let formatter = DateFormatter()
54-
formatter.dateFormat = "yyyy/MM/dd HH:mm"
55-
formatter.timeZone = TimeZone(identifier: "UTC")
56-
return formatter
71+
return Calendar.current.date(from: components)!
5772
}
5873

5974
}

0 commit comments

Comments
 (0)