moon_phase_tracker 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +272 -98
- data/Rakefile +8 -8
- data/examples/eight_phases_example.rb +100 -100
- data/examples/usage_example.rb +74 -74
- data/lib/moon_phase_tracker/lunar_calculator.rb +96 -0
- data/lib/moon_phase_tracker/phase/formatter.rb +4 -1
- data/lib/moon_phase_tracker/phase/parser.rb +7 -2
- data/lib/moon_phase_tracker/phase.rb +22 -4
- data/lib/moon_phase_tracker/phase_calculator/cycle_estimator.rb +1 -1
- data/lib/moon_phase_tracker/phase_calculator/phase_interpolator.rb +1 -1
- data/lib/moon_phase_tracker/tracker.rb +18 -0
- data/lib/moon_phase_tracker/version.rb +1 -1
- data/lib/moon_phase_tracker.rb +17 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c9587009682cc81b906d04054af17b88ae9863ebc56227457d507a2e484eb9c
|
|
4
|
+
data.tar.gz: 9a55ae1b9ffc3920f0fa7b9096bb5f107980efe55f646caf4d6f104890c51abb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '04509c13863e69615e2a4af359e997e9481fd3765e0950fe3a966a25d5f3bca4609607fbed3a1b5d2e32b375887e97ac4ec82d937224889988842b639ab26fc7'
|
|
7
|
+
data.tar.gz: 890dd9e2ec203dd22b512f50e7e5831ec7f18932ded680366627883c42307dcc3cc4f7e2009e44783b98e77791de51f34ca44512534375d7437b3b73f7c5a313
|
data/README.md
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
|
-
# MoonPhaseTracker
|
|
1
|
+
# 🌙 MoonPhaseTracker - Your Cosmic Calendar Companion
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
*"Because every developer deserves to howl at the right moon."*
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Ever wondered when to deploy your code for maximum lunar luck? Or perhaps you need to schedule that midnight debugging session during a proper New Moon? This delightful Ruby gem connects you to the cosmic dance above, using the official US Naval Observatory (USNO) API to bring celestial timing to your fingertips.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Whether you're building a werewolf scheduling app, a vampire calendar, or just want to impress your users with lunar-powered features, MoonPhaseTracker has got your back under every phase of the moon.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- 🌒 Complete 8-phase lunar cycle support (4 major + 4 intermediate phases)
|
|
11
|
-
- 🌓 Simple and intuitive interface
|
|
12
|
-
- 🌔 Accurate data from the official USNO Navy API
|
|
13
|
-
- 🌕 Visual representation with moon phase emojis
|
|
14
|
-
- 🌖 Interpolated intermediate phases for enhanced precision
|
|
15
|
-
- 🌗 Visual indicators for calculated vs official phases
|
|
16
|
-
- 🌘 Automatic caching to optimize requests
|
|
17
|
-
- ⚡ Robust error handling
|
|
18
|
-
- 🚦 Configurable rate limiting to respect API limits
|
|
9
|
+
**🎯 Accuracy: ~85-90%** *(Good enough for lunar calendars, probably too precise for howling)*
|
|
19
10
|
|
|
20
|
-
##
|
|
11
|
+
## ✨ What Makes This Gem Shine
|
|
12
|
+
|
|
13
|
+
- 🌑 **Time Travel Through Lunar Cycles** - Query moon phases by month, year, or from any date your heart desires
|
|
14
|
+
- 🌒 **The Full Lunar Symphony** - Complete 8-phase support (4 major crescendos + 4 melodic interludes)
|
|
15
|
+
- 🌓 **Simple as Moonlight** - An interface so intuitive, even a sleepy developer can use it at 3 AM
|
|
16
|
+
- 🌔 **Navy-Grade Precision** - Official USNO data because the Navy doesn't mess around with moon phases
|
|
17
|
+
- 🌕 **Emoji Magic** - Visual moon phases that spark joy in your terminal
|
|
18
|
+
- 🌖 **Mathematical Lunar Poetry** - Interpolated intermediate phases for when precision meets artistry
|
|
19
|
+
- 🌗 **Truth in Advertising** - Clear indicators showing official vs calculated phases (no moon phase imposters here!)
|
|
20
|
+
- 🌘 **Memory Like an Elephant** - Automatic caching because nobody likes waiting for the moon
|
|
21
|
+
- ⚡ **Bulletproof & Graceful** - Error handling that fails as elegantly as moonbeams
|
|
22
|
+
- 🚦 **API Etiquette** - Respectful rate limiting because even the Navy deserves a break
|
|
23
|
+
|
|
24
|
+
## 🚀 Summoning the Lunar Powers
|
|
25
|
+
|
|
26
|
+
*Ready to add some celestial magic to your Ruby project?*
|
|
21
27
|
|
|
22
28
|
Add this line to your application's Gemfile:
|
|
23
29
|
|
|
@@ -37,9 +43,13 @@ Or install it directly:
|
|
|
37
43
|
gem install moon_phase_tracker
|
|
38
44
|
```
|
|
39
45
|
|
|
40
|
-
##
|
|
46
|
+
## 🎭 Lunar Adventures Await
|
|
47
|
+
|
|
48
|
+
*Time to dance with the moon phases like a cosmic choreographer!*
|
|
41
49
|
|
|
42
|
-
###
|
|
50
|
+
### 🌟 The Essential Four - Your Lunar Greatest Hits
|
|
51
|
+
|
|
52
|
+
*Start your journey with the moon's main characters: the celestial quartet that's been stealing hearts since ancient times.*
|
|
43
53
|
|
|
44
54
|
```ruby
|
|
45
55
|
require 'moon_phase_tracker'
|
|
@@ -48,23 +58,26 @@ require 'moon_phase_tracker'
|
|
|
48
58
|
phases = MoonPhaseTracker.phases_for_month(2025, 8)
|
|
49
59
|
puts phases.first.to_s
|
|
50
60
|
# => "🌑 New Moon - 2025-08-04 at 11:13"
|
|
61
|
+
# (Perfect timing for new beginnings!)
|
|
51
62
|
|
|
52
|
-
# All phases for 2025
|
|
63
|
+
# All phases for 2025 - your yearly lunar roadmap
|
|
53
64
|
year_phases = MoonPhaseTracker.phases_for_year(2025)
|
|
54
65
|
|
|
55
|
-
# Next 6 phases from a specific date
|
|
66
|
+
# Next 6 phases from a specific date - peek into the future
|
|
56
67
|
future_phases = MoonPhaseTracker.phases_from_date("2025-08-01", 6)
|
|
57
68
|
```
|
|
58
69
|
|
|
59
|
-
###
|
|
70
|
+
### 🎨 The Full Lunar Canvas - All 8 Phases in Their Glory
|
|
71
|
+
|
|
72
|
+
*For the lunar perfectionists who want the complete story, not just the highlights reel.*
|
|
60
73
|
|
|
61
74
|
```ruby
|
|
62
75
|
require 'moon_phase_tracker'
|
|
63
76
|
|
|
64
|
-
# All 8 phases for August 2025
|
|
77
|
+
# All 8 phases for August 2025 - the complete lunar symphony
|
|
65
78
|
all_phases = MoonPhaseTracker.all_phases_for_month(2025, 8)
|
|
66
79
|
all_phases.each do |phase|
|
|
67
|
-
indicator = phase.interpolated ? "~ " : " "
|
|
80
|
+
indicator = phase.interpolated ? "~ " : " " # ~ means "calculated with love"
|
|
68
81
|
puts "#{indicator}#{phase}"
|
|
69
82
|
end
|
|
70
83
|
# => 🌑 New Moon - 2025-08-04 at 11:13
|
|
@@ -76,48 +89,96 @@ end
|
|
|
76
89
|
# => 🌗 Last Quarter - 2025-08-26 at 09:26
|
|
77
90
|
# => ~ 🌘 Waning Crescent - 2025-08-29 at 02:56
|
|
78
91
|
|
|
79
|
-
# All 8 phases for entire year
|
|
92
|
+
# All 8 phases for entire year - your cosmic annual planner
|
|
80
93
|
all_year_phases = MoonPhaseTracker.all_phases_for_year(2025)
|
|
81
94
|
|
|
82
|
-
# All 8 phases from specific date (2 lunar cycles)
|
|
95
|
+
# All 8 phases from specific date (2 lunar cycles) - the extended edition
|
|
83
96
|
extended_phases = MoonPhaseTracker.all_phases_from_date("2025-08-01", 2)
|
|
84
97
|
```
|
|
85
98
|
|
|
86
|
-
###
|
|
99
|
+
### 🔮 Instant Moon Phase - No API, No Waiting
|
|
100
|
+
|
|
101
|
+
*What phase is the moon right now? How bright is it? Pure math, instant answer.*
|
|
102
|
+
|
|
103
|
+
These methods use a synodic month calculation — no API calls, no network, no rate limits. Perfect for display UIs and real-time widgets.
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
require 'moon_phase_tracker'
|
|
107
|
+
|
|
108
|
+
# What's the moon doing right now?
|
|
109
|
+
phase = MoonPhaseTracker.current_phase
|
|
110
|
+
puts phase.to_s
|
|
111
|
+
# => "🌔 Waxing Gibbous - 2025-08-15 at 14:30"
|
|
112
|
+
puts phase.illumination # => 78.5 (percent)
|
|
113
|
+
puts phase.lunar_age # => 12.3 (days since last new moon)
|
|
114
|
+
puts phase.source # => :calculated
|
|
115
|
+
|
|
116
|
+
# Phase at any date/time - past or future
|
|
117
|
+
phase = MoonPhaseTracker.phase_at(Time.utc(2025, 6, 11, 7, 44))
|
|
118
|
+
puts phase.name # => "Full Moon"
|
|
119
|
+
puts phase.illumination # => ~100.0
|
|
120
|
+
|
|
121
|
+
# Just the illumination percentage
|
|
122
|
+
illum = MoonPhaseTracker.illumination(Date.today)
|
|
123
|
+
puts "#{illum.round(1)}% illuminated"
|
|
124
|
+
|
|
125
|
+
# Works with Date, Time, DateTime, or String
|
|
126
|
+
MoonPhaseTracker.phase_at(Date.new(2025, 1, 15))
|
|
127
|
+
MoonPhaseTracker.phase_at("2025-01-15")
|
|
128
|
+
MoonPhaseTracker.phase_at(DateTime.now)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
> **Accuracy note:** The synodic model gives the correct named phase ~99% of the time. Within ~6 hours of a phase boundary, it may differ from USNO API data. The API path gives precision for scheduling; the calculator path gives instant answers for display.
|
|
132
|
+
|
|
133
|
+
### 🎯 The Tracker Class - Your Personal Lunar Assistant
|
|
134
|
+
|
|
135
|
+
*Think of it as your moon phase butler, always ready to serve up cosmic timing with a bow tie.*
|
|
87
136
|
|
|
88
137
|
```ruby
|
|
89
138
|
tracker = MoonPhaseTracker::Tracker.new
|
|
90
139
|
|
|
91
|
-
# Automatic formatting for display
|
|
140
|
+
# Automatic formatting for display - because presentation matters
|
|
92
141
|
august_phases = tracker.phases_for_month(2025, 8)
|
|
93
142
|
formatted = tracker.format_phases(august_phases, "August Phases")
|
|
94
143
|
puts formatted
|
|
95
144
|
|
|
96
|
-
# Next moon phase
|
|
145
|
+
# Next moon phase - your cosmic fortune telling
|
|
97
146
|
next_phase = tracker.next_phase
|
|
98
147
|
puts next_phase.to_s
|
|
99
148
|
|
|
100
|
-
# Current month phases
|
|
149
|
+
# Current month phases - what's happening in your lunar neighborhood
|
|
101
150
|
current_month = tracker.current_month_phases
|
|
151
|
+
|
|
152
|
+
# Instant phase at any date - no API call needed
|
|
153
|
+
phase = tracker.phase_at("2025-06-11")
|
|
154
|
+
puts phase.illumination # => Float (percent)
|
|
155
|
+
|
|
156
|
+
# Just the illumination number
|
|
157
|
+
illum = tracker.illumination("2025-06-11")
|
|
158
|
+
|
|
159
|
+
# Current phase right now
|
|
160
|
+
puts tracker.current_phase
|
|
102
161
|
```
|
|
103
162
|
|
|
104
|
-
###
|
|
163
|
+
### 🔍 Getting Personal with Moon Phases
|
|
164
|
+
|
|
165
|
+
*Each phase has its own personality - let's get to know them intimately.*
|
|
105
166
|
|
|
106
167
|
```ruby
|
|
107
168
|
phase = phases.first
|
|
108
169
|
|
|
109
170
|
# Phase information
|
|
110
171
|
puts phase.name # "New Moon"
|
|
111
|
-
puts phase.symbol # "🌑"
|
|
172
|
+
puts phase.symbol # "🌑" (isn't it beautiful?)
|
|
112
173
|
puts phase.formatted_date # "2025-08-04"
|
|
113
|
-
puts phase.formatted_time # "11:13"
|
|
114
|
-
puts phase.interpolated # false (
|
|
174
|
+
puts phase.formatted_time # "11:13" (UTC - the moon doesn't do timezones)
|
|
175
|
+
puts phase.interpolated # false (certified genuine USNO data)
|
|
115
176
|
|
|
116
|
-
# Working with interpolated phases
|
|
177
|
+
# Working with interpolated phases - the mathematically gifted ones
|
|
117
178
|
interpolated_phase = all_phases.find(&:interpolated)
|
|
118
|
-
puts interpolated_phase.interpolated # true (calculated)
|
|
179
|
+
puts interpolated_phase.interpolated # true (calculated with mathematical precision)
|
|
119
180
|
puts interpolated_phase.name # "Waxing Crescent"
|
|
120
|
-
puts interpolated_phase.symbol # "🌒"
|
|
181
|
+
puts interpolated_phase.symbol # "🌒" (still adorable)
|
|
121
182
|
|
|
122
183
|
# Complete hash with all data
|
|
123
184
|
details = phase.to_h
|
|
@@ -129,94 +190,191 @@ details = phase.to_h
|
|
|
129
190
|
# symbol: "🌑",
|
|
130
191
|
# iso_date: "2025-08-04",
|
|
131
192
|
# utc_time: "2025-08-04T11:13:00Z",
|
|
132
|
-
# interpolated: false
|
|
193
|
+
# interpolated: false,
|
|
194
|
+
# source: :api, # :api, :interpolated, or :calculated
|
|
195
|
+
# illumination: nil, # Float for calculated phases, nil for API
|
|
196
|
+
# lunar_age: nil # Float for calculated phases, nil for API
|
|
133
197
|
# }
|
|
134
198
|
|
|
135
|
-
# Date checks
|
|
136
|
-
phase.in_month?(2025, 8) # => true
|
|
137
|
-
phase.in_year?(2025) # => true
|
|
199
|
+
# Date checks - lunar detective work
|
|
200
|
+
phase.in_month?(2025, 8) # => true (August moon confirmed!)
|
|
201
|
+
phase.in_year?(2025) # => true (definitely a 2025 vintage)
|
|
138
202
|
```
|
|
139
203
|
|
|
140
|
-
###
|
|
204
|
+
### 🛡️ When the Moon Plays Hard to Get
|
|
205
|
+
|
|
206
|
+
*Even celestial bodies have bad days. Here's how to handle lunar tantrums gracefully.*
|
|
141
207
|
|
|
142
208
|
```ruby
|
|
143
209
|
begin
|
|
144
210
|
phases = MoonPhaseTracker.phases_for_month(2025, 8)
|
|
145
211
|
rescue MoonPhaseTracker::NetworkError => e
|
|
146
|
-
puts "Network
|
|
212
|
+
puts "Network hiccup: #{e.message}" # The internet is having a moment
|
|
147
213
|
rescue MoonPhaseTracker::APIError => e
|
|
148
|
-
puts "API
|
|
214
|
+
puts "API tantrum: #{e.message}" # The Navy API is feeling moody
|
|
149
215
|
rescue MoonPhaseTracker::InvalidDateError => e
|
|
150
|
-
puts "
|
|
216
|
+
puts "Time travel error: #{e.message}" # That date doesn't exist in this dimension
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 🎪 Real-World Lunar Magic
|
|
221
|
+
|
|
222
|
+
*Because theory is nice, but seeing the moon phases in action is where the real magic happens.*
|
|
223
|
+
|
|
224
|
+
#### Hybrid Architecture: USNO for Scheduling, Calculator for Display
|
|
225
|
+
|
|
226
|
+
The API gives you exact dates. The calculator gives you instant answers. Use both — a background job populates a `lunar_phases` table with USNO data, and `LunarCalculator` handles the real-time display layer.
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
# app/jobs/sync_lunar_phases_job.rb
|
|
230
|
+
class SyncLunarPhasesJob < ApplicationJob
|
|
231
|
+
def perform(year = Date.current.year)
|
|
232
|
+
phases = MoonPhaseTracker.all_phases_for_year(year)
|
|
233
|
+
|
|
234
|
+
phases.each do |phase|
|
|
235
|
+
LunarPhase.upsert(
|
|
236
|
+
{
|
|
237
|
+
name: phase.name,
|
|
238
|
+
phase_type: phase.phase_type.to_s,
|
|
239
|
+
exact_at: phase.to_h[:utc_time],
|
|
240
|
+
source: phase.source.to_s
|
|
241
|
+
},
|
|
242
|
+
unique_by: :exact_at
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# app/models/lunar_phase.rb
|
|
251
|
+
class LunarPhase < ApplicationRecord
|
|
252
|
+
scope :upcoming, -> { where("exact_at > ?", Time.current).order(:exact_at) }
|
|
253
|
+
scope :next_full_moon, -> { upcoming.where(phase_type: "full_moon").first }
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
# app/helpers/moon_helper.rb
|
|
259
|
+
module MoonHelper
|
|
260
|
+
def moon_badge(date = Date.current)
|
|
261
|
+
phase = MoonPhaseTracker.phase_at(date)
|
|
262
|
+
"#{phase.symbol} #{phase.illumination.round}%"
|
|
263
|
+
end
|
|
151
264
|
end
|
|
152
265
|
```
|
|
153
266
|
|
|
154
|
-
|
|
267
|
+
Two data sources, each doing what they do best. The table owns scheduling precision (notifications, rituals, content triggers). The calculator owns display (emoji, illumination percentage, "what phase is it right now?"). They never step on each other's toes.
|
|
268
|
+
|
|
269
|
+
See the `examples/usage_example.rb` and `examples/eight_phases_example.rb` files for more usage examples.
|
|
155
270
|
|
|
156
|
-
|
|
271
|
+
## 📚 The Lunar Grimoire - Complete API Spellbook
|
|
157
272
|
|
|
158
|
-
|
|
273
|
+
*Your comprehensive guide to all the lunar incantations at your disposal.*
|
|
159
274
|
|
|
160
|
-
###
|
|
275
|
+
### 🎭 The Core Cast - 4 Major Phase Methods
|
|
276
|
+
|
|
277
|
+
*The essential methods that'll get you 80% of your lunar needs covered.*
|
|
161
278
|
|
|
162
279
|
- `MoonPhaseTracker.phases_for_month(year, month)` - Major phases for a specific month
|
|
163
280
|
- `MoonPhaseTracker.phases_for_year(year)` - All major phases for a year
|
|
164
281
|
- `MoonPhaseTracker.phases_from_date(date, num_phases)` - Major phases from a specific date
|
|
165
282
|
|
|
166
|
-
###
|
|
283
|
+
### 🔮 Instant Lookup - No API Required
|
|
284
|
+
|
|
285
|
+
*Pure math, zero latency. For when you need the moon right now.*
|
|
286
|
+
|
|
287
|
+
- `MoonPhaseTracker.phase_at(date)` - Phase at any date/time (returns `Phase` with illumination)
|
|
288
|
+
- `MoonPhaseTracker.illumination(date)` - Illumination percentage (returns `Float` 0..100)
|
|
289
|
+
- `MoonPhaseTracker.current_phase` - Phase right now (shortcut for `phase_at(Time.now.utc)`)
|
|
290
|
+
|
|
291
|
+
No API calls — pure math under the hood.
|
|
292
|
+
|
|
293
|
+
### 🌈 The Full Spectrum - 8-Phase Methods for Lunar Completionists
|
|
294
|
+
|
|
295
|
+
*For those who believe in doing things thoroughly (and slightly obsessively).*
|
|
167
296
|
|
|
168
297
|
- `MoonPhaseTracker.all_phases_for_month(year, month)` - All 8 phases for a specific month
|
|
169
298
|
- `MoonPhaseTracker.all_phases_for_year(year)` - All 8 phases for a year
|
|
170
299
|
- `MoonPhaseTracker.all_phases_from_date(date, num_cycles)` - All 8 phases from a specific date
|
|
171
300
|
|
|
172
|
-
###
|
|
301
|
+
### 🎨 Meet the Lunar Cast - All 8 Phase Personalities
|
|
302
|
+
|
|
303
|
+
#### 🏆 The Main Characters (Official USNO Data)
|
|
304
|
+
*These are the A-listers - certified by the Navy, guaranteed to impress.*
|
|
305
|
+
- 🌑 `:new_moon` - New Moon *(The mysterious beginning)*
|
|
306
|
+
- 🌓 `:first_quarter` - First Quarter *(Half-lit and growing strong)*
|
|
307
|
+
- 🌕 `:full_moon` - Full Moon *(The showstopper, the main event)*
|
|
308
|
+
- 🌗 `:last_quarter` - Last Quarter *(Gracefully waning)*
|
|
309
|
+
|
|
310
|
+
#### 🎭 The Supporting Cast (Interpolated ~85-90% accuracy)
|
|
311
|
+
*The understudies that complete the story - mathematically calculated with love.*
|
|
312
|
+
- 🌒 `:waxing_crescent` - Waxing Crescent *(The optimistic youngster)*
|
|
313
|
+
- 🌔 `:waxing_gibbous` - Waxing Gibbous *(Almost there, building anticipation)*
|
|
314
|
+
- 🌖 `:waning_gibbous` - Waning Gibbous *(The wise elder, still radiant)*
|
|
315
|
+
- 🌘 `:waning_crescent` - Waning Crescent *(The gentle farewell)*
|
|
316
|
+
|
|
317
|
+
### 🏷️ Phase Sources - Know Where Your Data Comes From
|
|
318
|
+
|
|
319
|
+
*Every phase carries a passport stamped with its origin story.*
|
|
173
320
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
321
|
+
| Source | Meaning | When |
|
|
322
|
+
|--------|---------|------|
|
|
323
|
+
| `:api` | Official USNO data | `phases_for_month`, `phases_for_year`, etc. |
|
|
324
|
+
| `:interpolated` | Calculated between two API phases | `all_phases_for_month`, `all_phases_for_year`, etc. |
|
|
325
|
+
| `:calculated` | Pure synodic math model | `phase_at`, `illumination`, `current_phase` |
|
|
179
326
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
327
|
+
```ruby
|
|
328
|
+
phase = MoonPhaseTracker.phase_at(Date.today)
|
|
329
|
+
phase.source # => :calculated
|
|
330
|
+
phase.illumination # => 65.3 (percent)
|
|
331
|
+
phase.lunar_age # => 10.2 (days since last new moon)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## ⚓ Our Trusted Cosmic Oracle
|
|
335
|
+
|
|
336
|
+
*We get our lunar wisdom from the finest source - the folks who navigate the seven seas by the stars.*
|
|
185
337
|
|
|
186
|
-
|
|
338
|
+
This gem uses the official US Naval Observatory (USNO) API - because when it comes to celestial navigation, you want the folks who've been steering ships by the stars for centuries:
|
|
187
339
|
|
|
188
|
-
This gem uses the official US Naval Observatory (USNO) API:
|
|
189
340
|
- **Base URL**: https://aa.usno.navy.mil/api/moon/phases/
|
|
190
|
-
- **API Version**: 4.0.1
|
|
341
|
+
- **API Version**: 4.0.1 (battle-tested and Navy-approved)
|
|
191
342
|
- **Documentation**: https://aa.usno.navy.mil/data/api
|
|
192
343
|
|
|
193
|
-
All times are provided in Universal Time (UTC).
|
|
344
|
+
All times are provided in Universal Time (UTC) - because the moon doesn't care about your timezone preferences, and neither should precise astronomical data.
|
|
194
345
|
|
|
195
|
-
## Rate Limiting
|
|
346
|
+
## 🚦 Playing Nice with the Navy - Rate Limiting with Style
|
|
196
347
|
|
|
197
|
-
|
|
348
|
+
*Because even cosmic APIs need coffee breaks, and we're not monsters.*
|
|
198
349
|
|
|
199
|
-
|
|
350
|
+
Think of rate limiting as the polite pause between questions when chatting with a wise oracle. We implement respectful rate limiting to keep the USNO Navy API happy (and responsive). By default, we're as patient as a monk - **1 request per second** with the restraint of a zen master.
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
### 🧘 The Zen Approach - Default Rate Limiting
|
|
354
|
+
|
|
355
|
+
*Slow and steady wins the lunar race.*
|
|
200
356
|
|
|
201
357
|
```ruby
|
|
202
|
-
# Default: 1 request per second
|
|
358
|
+
# Default: 1 request per second - the patient approach
|
|
203
359
|
tracker = MoonPhaseTracker::Tracker.new
|
|
204
360
|
|
|
205
|
-
# Check current rate limit configuration
|
|
361
|
+
# Check current rate limit configuration - know thy limits
|
|
206
362
|
puts tracker.rate_limit_info
|
|
207
363
|
# => {:requests_per_second=>1.0, :burst_size=>1, :available_tokens=>1}
|
|
208
364
|
|
|
209
|
-
# Multiple requests will be automatically rate limited
|
|
365
|
+
# Multiple requests will be automatically rate limited - watch the magic
|
|
210
366
|
start = Time.now
|
|
211
|
-
tracker.phases_for_year(2025) # Immediate
|
|
212
|
-
tracker.phases_for_year(2024) # Waits ~1 second
|
|
367
|
+
tracker.phases_for_year(2025) # Immediate (first one's free!)
|
|
368
|
+
tracker.phases_for_year(2024) # Waits ~1 second (patience, young padawan)
|
|
213
369
|
puts "Total time: #{Time.now - start}s" # ~1.0 seconds
|
|
214
370
|
```
|
|
215
371
|
|
|
216
|
-
### Custom Rate Limiting
|
|
372
|
+
### ⚡ Need for Speed? - Custom Rate Limiting
|
|
373
|
+
|
|
374
|
+
*For when you want to live life in the fast lane (but still be respectful).*
|
|
217
375
|
|
|
218
376
|
```ruby
|
|
219
|
-
# Create custom rate limiter: 3 requests per second, burst of 2
|
|
377
|
+
# Create custom rate limiter: 3 requests per second, burst of 2 - living a little
|
|
220
378
|
rate_limiter = MoonPhaseTracker::RateLimiter.new(
|
|
221
379
|
requests_per_second: 3.0,
|
|
222
380
|
burst_size: 2
|
|
@@ -224,16 +382,18 @@ rate_limiter = MoonPhaseTracker::RateLimiter.new(
|
|
|
224
382
|
|
|
225
383
|
tracker = MoonPhaseTracker::Tracker.new(rate_limiter: rate_limiter)
|
|
226
384
|
|
|
227
|
-
# Burst requests are immediate, then rate limited
|
|
228
|
-
tracker.phases_for_year(2025) # Immediate
|
|
229
|
-
tracker.phases_for_year(2024) # Immediate (
|
|
230
|
-
tracker.phases_for_year(2023) # Waits ~0.33 seconds
|
|
385
|
+
# Burst requests are immediate, then rate limited - controlled excitement
|
|
386
|
+
tracker.phases_for_year(2025) # Immediate (wheee!)
|
|
387
|
+
tracker.phases_for_year(2024) # Immediate (double wheee!)
|
|
388
|
+
tracker.phases_for_year(2023) # Waits ~0.33 seconds (and... breathe)
|
|
231
389
|
```
|
|
232
390
|
|
|
233
|
-
### Environment
|
|
391
|
+
### 🌍 Set It and Forget It - Environment Variables
|
|
392
|
+
|
|
393
|
+
*Configure once, smile forever.*
|
|
234
394
|
|
|
235
395
|
```ruby
|
|
236
|
-
# Set via environment variables
|
|
396
|
+
# Set via environment variables - the lazy developer's paradise
|
|
237
397
|
ENV["MOON_PHASE_RATE_LIMIT"] = "2.5"
|
|
238
398
|
ENV["MOON_PHASE_BURST_SIZE"] = "3"
|
|
239
399
|
|
|
@@ -242,20 +402,24 @@ puts tracker.rate_limit_info
|
|
|
242
402
|
# => {:requests_per_second=>2.5, :burst_size=>3, :available_tokens=>3}
|
|
243
403
|
```
|
|
244
404
|
|
|
245
|
-
### Disabling Rate Limiting
|
|
405
|
+
### 🏁 Living Dangerously - Disabling Rate Limiting
|
|
406
|
+
|
|
407
|
+
*For the rebels and the reckless (use responsibly, dear lunar adventurer).*
|
|
246
408
|
|
|
247
409
|
```ruby
|
|
248
|
-
# Disable rate limiting completely
|
|
410
|
+
# Disable rate limiting completely - for the speed demons
|
|
249
411
|
ENV["MOON_PHASE_RATE_LIMIT"] = "0"
|
|
250
412
|
|
|
251
413
|
tracker = MoonPhaseTracker::Tracker.new
|
|
252
|
-
puts tracker.rate_limit_info # => nil
|
|
414
|
+
puts tracker.rate_limit_info # => nil (no limits, no safety net!)
|
|
253
415
|
|
|
254
|
-
# Or pass nil explicitly
|
|
416
|
+
# Or pass nil explicitly - the explicit rebel
|
|
255
417
|
tracker = MoonPhaseTracker::Tracker.new(rate_limiter: nil)
|
|
256
418
|
```
|
|
257
419
|
|
|
258
|
-
### Rate Limit Monitoring
|
|
420
|
+
### 🔬 Under the Hood - Rate Limit Monitoring
|
|
421
|
+
|
|
422
|
+
*For the control freaks and performance enthusiasts among us.*
|
|
259
423
|
|
|
260
424
|
```ruby
|
|
261
425
|
rate_limiter = MoonPhaseTracker::RateLimiter.new(
|
|
@@ -263,18 +427,20 @@ rate_limiter = MoonPhaseTracker::RateLimiter.new(
|
|
|
263
427
|
burst_size: 2
|
|
264
428
|
)
|
|
265
429
|
|
|
266
|
-
# Check if request can proceed without waiting
|
|
267
|
-
puts rate_limiter.can_proceed? # => true
|
|
430
|
+
# Check if request can proceed without waiting - the crystal ball
|
|
431
|
+
puts rate_limiter.can_proceed? # => true (or false if you've been naughty)
|
|
268
432
|
|
|
269
|
-
# Check current token status
|
|
433
|
+
# Check current token status - your digital wallet
|
|
270
434
|
puts rate_limiter.configuration
|
|
271
435
|
# => {:requests_per_second=>1.0, :burst_size=>2, :available_tokens=>2}
|
|
272
436
|
|
|
273
|
-
# Manual rate limiting control
|
|
274
|
-
rate_limiter.throttle # Waits if necessary and consumes token
|
|
437
|
+
# Manual rate limiting control - take the wheel
|
|
438
|
+
rate_limiter.throttle # Waits if necessary and consumes token (om nom nom)
|
|
275
439
|
```
|
|
276
440
|
|
|
277
|
-
### Token Bucket Algorithm
|
|
441
|
+
### 🪣 The Magic Behind the Curtain - Token Bucket Algorithm
|
|
442
|
+
|
|
443
|
+
*The elegant dance of digital tokens that keeps everything flowing smoothly.*
|
|
278
444
|
|
|
279
445
|
The rate limiter uses a token bucket algorithm:
|
|
280
446
|
|
|
@@ -283,20 +449,28 @@ The rate limiter uses a token bucket algorithm:
|
|
|
283
449
|
- **Refill Rate**: How quickly tokens are replenished
|
|
284
450
|
- **Thread Safe**: Handles concurrent requests safely
|
|
285
451
|
|
|
286
|
-
## Development
|
|
452
|
+
## 🛠️ Join the Lunar Development Cult
|
|
287
453
|
|
|
288
|
-
|
|
454
|
+
*Want to contribute to the cosmic cause? We welcome fellow moon enthusiasts!*
|
|
289
455
|
|
|
290
|
-
|
|
456
|
+
After checking out the repo, run `bin/setup` to install dependencies (like preparing your lunar laboratory). Then, run `rake spec` to run the tests (because even moon phases need quality control). You can also run `bin/console` for an interactive prompt that will allow you to experiment (perfect for midnight coding sessions under the actual moon).
|
|
291
457
|
|
|
292
|
-
|
|
458
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org) (sharing lunar magic with the world!).
|
|
293
459
|
|
|
294
|
-
|
|
460
|
+
## 🤝 Become a Lunar Contributor
|
|
295
461
|
|
|
296
|
-
|
|
462
|
+
*Every great gem needs a community of stargazers and code wizards.*
|
|
463
|
+
|
|
464
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/dklima/moon_phase_tracker. Found a bug? Think of it as discovering a new lunar crater - exciting stuff! This project is intended to be a safe, welcoming space for collaboration, where all contributors can shine as bright as a Full Moon. Contributors are expected to adhere to the [code of conduct](https://github.com/dklima/moon_phase_tracker/blob/main/CODE_OF_CONDUCT.md).
|
|
465
|
+
|
|
466
|
+
## 📜 Legal Lunar Stuff
|
|
467
|
+
|
|
468
|
+
*Even moon phases need proper paperwork.*
|
|
297
469
|
|
|
298
470
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
299
471
|
|
|
300
|
-
##
|
|
472
|
+
## 🌟 Lunar Community Guidelines
|
|
473
|
+
|
|
474
|
+
*We believe in creating a space as welcoming as moonlight on a summer evening.*
|
|
301
475
|
|
|
302
|
-
Everyone interacting in the MoonPhaseTracker project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/dklima/moon_phase_tracker/blob/main/CODE_OF_CONDUCT.md).
|
|
476
|
+
Everyone interacting in the MoonPhaseTracker project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/dklima/moon_phase_tracker/blob/main/CODE_OF_CONDUCT.md). Together, we create a community as harmonious as the lunar cycles themselves.
|
data/Rakefile
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'bundler/gem_tasks'
|
|
4
|
-
require 'rspec/core/rake_task'
|
|
5
|
-
|
|
6
|
-
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
-
|
|
8
|
-
task default: :spec
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
task default: :spec
|
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# Add the lib directory to the load path
|
|
5
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
6
|
-
|
|
7
|
-
require 'moon_phase_tracker'
|
|
8
|
-
|
|
9
|
-
puts '=== Moon Phase Tracker - 8 Phases Example ==='
|
|
10
|
-
puts 'Demonstrating all 8 lunar phases (4 major + 4 intermediate)'
|
|
11
|
-
puts ''
|
|
12
|
-
|
|
13
|
-
begin
|
|
14
|
-
# Example 1: Get all 8 phases for August 2025
|
|
15
|
-
puts '🌙 All phases for August 2025:'
|
|
16
|
-
puts '=' * 40
|
|
17
|
-
phases = MoonPhaseTracker.all_phases_for_month(2025, 8)
|
|
18
|
-
|
|
19
|
-
if phases.any?
|
|
20
|
-
phases.each do |phase|
|
|
21
|
-
prefix = phase.interpolated ? '~ ' : ' '
|
|
22
|
-
puts "#{prefix}#{phase}"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
major_count = phases.count { |p| !p.interpolated }
|
|
26
|
-
interpolated_count = phases.count(&:interpolated)
|
|
27
|
-
|
|
28
|
-
puts ''
|
|
29
|
-
puts "Total: #{phases.size} phases (#{major_count} major, #{interpolated_count} interpolated)"
|
|
30
|
-
puts '~ indicates interpolated phases'
|
|
31
|
-
else
|
|
32
|
-
puts 'No phases found for this period.'
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
puts ''
|
|
36
|
-
puts '-' * 50
|
|
37
|
-
puts ''
|
|
38
|
-
|
|
39
|
-
# Example 2: Compare 4 vs 8 phases
|
|
40
|
-
puts '🔍 Comparison: 4 Major Phases vs 8 Complete Phases'
|
|
41
|
-
puts '=' * 55
|
|
42
|
-
|
|
43
|
-
major_phases = MoonPhaseTracker.phases_for_month(2025, 8)
|
|
44
|
-
all_phases = MoonPhaseTracker.all_phases_for_month(2025, 8)
|
|
45
|
-
|
|
46
|
-
puts "Major phases only (#{major_phases.size}):"
|
|
47
|
-
major_phases.each { |phase| puts " #{phase}" }
|
|
48
|
-
|
|
49
|
-
puts ''
|
|
50
|
-
puts "All phases including interpolated (#{all_phases.size}):"
|
|
51
|
-
all_phases.each do |phase|
|
|
52
|
-
prefix = phase.interpolated ? '~ ' : ' '
|
|
53
|
-
puts "#{prefix}#{phase}"
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
puts ''
|
|
57
|
-
puts '-' * 50
|
|
58
|
-
puts ''
|
|
59
|
-
|
|
60
|
-
# Example 3: Show phase symbols
|
|
61
|
-
puts '🎭 Phase Symbols Reference'
|
|
62
|
-
puts '=' * 30
|
|
63
|
-
MoonPhaseTracker::Phase::PHASE_SYMBOLS.each do |type, symbol|
|
|
64
|
-
name = type.to_s.split('_').map(&:capitalize).join(' ')
|
|
65
|
-
puts "#{symbol} #{name}"
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
puts ''
|
|
69
|
-
puts '-' * 50
|
|
70
|
-
puts ''
|
|
71
|
-
|
|
72
|
-
# Example 4: Get phases from a specific date with 8 phases
|
|
73
|
-
puts '📅 8 Phases from a specific date (2025-08-01, 2 cycles)'
|
|
74
|
-
puts '=' * 60
|
|
75
|
-
date_phases = MoonPhaseTracker.all_phases_from_date('2025-08-01', 2)
|
|
76
|
-
|
|
77
|
-
if date_phases.any?
|
|
78
|
-
date_phases.each do |phase|
|
|
79
|
-
prefix = phase.interpolated ? '~ ' : ' '
|
|
80
|
-
puts "#{prefix}#{phase}"
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
major_count = date_phases.count { |p| !p.interpolated }
|
|
84
|
-
interpolated_count = date_phases.count(&:interpolated)
|
|
85
|
-
|
|
86
|
-
puts ''
|
|
87
|
-
puts "Total: #{date_phases.size} phases over 2 lunar cycles"
|
|
88
|
-
puts "(#{major_count} major, #{interpolated_count} interpolated)"
|
|
89
|
-
end
|
|
90
|
-
rescue MoonPhaseTracker::Error => e
|
|
91
|
-
puts "Error: #{e.message}"
|
|
92
|
-
rescue StandardError => e
|
|
93
|
-
puts "Unexpected error: #{e.message}"
|
|
94
|
-
puts 'This example requires an active internet connection to fetch moon phase data.'
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
puts ''
|
|
98
|
-
puts 'Note: This example uses real astronomical data from the US Naval Observatory.'
|
|
99
|
-
puts 'Interpolated phases (~) are calculated estimates between official phases.'
|
|
100
|
-
puts 'All dates are in ISO 8601 format (YYYY-MM-DD) and times are in UTC.'
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Add the lib directory to the load path
|
|
5
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
6
|
+
|
|
7
|
+
require 'moon_phase_tracker'
|
|
8
|
+
|
|
9
|
+
puts '=== Moon Phase Tracker - 8 Phases Example ==='
|
|
10
|
+
puts 'Demonstrating all 8 lunar phases (4 major + 4 intermediate)'
|
|
11
|
+
puts ''
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
# Example 1: Get all 8 phases for August 2025
|
|
15
|
+
puts '🌙 All phases for August 2025:'
|
|
16
|
+
puts '=' * 40
|
|
17
|
+
phases = MoonPhaseTracker.all_phases_for_month(2025, 8)
|
|
18
|
+
|
|
19
|
+
if phases.any?
|
|
20
|
+
phases.each do |phase|
|
|
21
|
+
prefix = phase.interpolated ? '~ ' : ' '
|
|
22
|
+
puts "#{prefix}#{phase}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
major_count = phases.count { |p| !p.interpolated }
|
|
26
|
+
interpolated_count = phases.count(&:interpolated)
|
|
27
|
+
|
|
28
|
+
puts ''
|
|
29
|
+
puts "Total: #{phases.size} phases (#{major_count} major, #{interpolated_count} interpolated)"
|
|
30
|
+
puts '~ indicates interpolated phases'
|
|
31
|
+
else
|
|
32
|
+
puts 'No phases found for this period.'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
puts ''
|
|
36
|
+
puts '-' * 50
|
|
37
|
+
puts ''
|
|
38
|
+
|
|
39
|
+
# Example 2: Compare 4 vs 8 phases
|
|
40
|
+
puts '🔍 Comparison: 4 Major Phases vs 8 Complete Phases'
|
|
41
|
+
puts '=' * 55
|
|
42
|
+
|
|
43
|
+
major_phases = MoonPhaseTracker.phases_for_month(2025, 8)
|
|
44
|
+
all_phases = MoonPhaseTracker.all_phases_for_month(2025, 8)
|
|
45
|
+
|
|
46
|
+
puts "Major phases only (#{major_phases.size}):"
|
|
47
|
+
major_phases.each { |phase| puts " #{phase}" }
|
|
48
|
+
|
|
49
|
+
puts ''
|
|
50
|
+
puts "All phases including interpolated (#{all_phases.size}):"
|
|
51
|
+
all_phases.each do |phase|
|
|
52
|
+
prefix = phase.interpolated ? '~ ' : ' '
|
|
53
|
+
puts "#{prefix}#{phase}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts ''
|
|
57
|
+
puts '-' * 50
|
|
58
|
+
puts ''
|
|
59
|
+
|
|
60
|
+
# Example 3: Show phase symbols
|
|
61
|
+
puts '🎭 Phase Symbols Reference'
|
|
62
|
+
puts '=' * 30
|
|
63
|
+
MoonPhaseTracker::Phase::PHASE_SYMBOLS.each do |type, symbol|
|
|
64
|
+
name = type.to_s.split('_').map(&:capitalize).join(' ')
|
|
65
|
+
puts "#{symbol} #{name}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
puts ''
|
|
69
|
+
puts '-' * 50
|
|
70
|
+
puts ''
|
|
71
|
+
|
|
72
|
+
# Example 4: Get phases from a specific date with 8 phases
|
|
73
|
+
puts '📅 8 Phases from a specific date (2025-08-01, 2 cycles)'
|
|
74
|
+
puts '=' * 60
|
|
75
|
+
date_phases = MoonPhaseTracker.all_phases_from_date('2025-08-01', 2)
|
|
76
|
+
|
|
77
|
+
if date_phases.any?
|
|
78
|
+
date_phases.each do |phase|
|
|
79
|
+
prefix = phase.interpolated ? '~ ' : ' '
|
|
80
|
+
puts "#{prefix}#{phase}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
major_count = date_phases.count { |p| !p.interpolated }
|
|
84
|
+
interpolated_count = date_phases.count(&:interpolated)
|
|
85
|
+
|
|
86
|
+
puts ''
|
|
87
|
+
puts "Total: #{date_phases.size} phases over 2 lunar cycles"
|
|
88
|
+
puts "(#{major_count} major, #{interpolated_count} interpolated)"
|
|
89
|
+
end
|
|
90
|
+
rescue MoonPhaseTracker::Error => e
|
|
91
|
+
puts "Error: #{e.message}"
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
puts "Unexpected error: #{e.message}"
|
|
94
|
+
puts 'This example requires an active internet connection to fetch moon phase data.'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
puts ''
|
|
98
|
+
puts 'Note: This example uses real astronomical data from the US Naval Observatory.'
|
|
99
|
+
puts 'Interpolated phases (~) are calculated estimates between official phases.'
|
|
100
|
+
puts 'All dates are in ISO 8601 format (YYYY-MM-DD) and times are in UTC.'
|
data/examples/usage_example.rb
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require_relative '../lib/moon_phase_tracker'
|
|
5
|
-
|
|
6
|
-
puts '=== Moon Phase Tracker - Usage Examples =='
|
|
7
|
-
puts
|
|
8
|
-
|
|
9
|
-
tracker = MoonPhaseTracker::Tracker.new
|
|
10
|
-
|
|
11
|
-
begin
|
|
12
|
-
puts '1. Moon phases for August 2025:'
|
|
13
|
-
puts '-' * 40
|
|
14
|
-
august_phases = MoonPhaseTracker.phases_for_month(2025, 8)
|
|
15
|
-
puts tracker.format_phases(august_phases, "#{MoonPhaseTracker::Tracker.month_name(8)} 2025 Phases")
|
|
16
|
-
puts
|
|
17
|
-
|
|
18
|
-
puts '2. All moon phases in 2025 (first 8):'
|
|
19
|
-
puts '-' * 50
|
|
20
|
-
year_phases = MoonPhaseTracker.phases_for_year(2025)
|
|
21
|
-
puts tracker.format_phases(year_phases.first(8), 'First 8 phases of 2025')
|
|
22
|
-
puts
|
|
23
|
-
|
|
24
|
-
puts '3. Next 6 phases starting from 2025-08-01:'
|
|
25
|
-
puts '-' * 45
|
|
26
|
-
future_phases = MoonPhaseTracker.phases_from_date('2025-08-01', 6)
|
|
27
|
-
puts tracker.format_phases(future_phases, 'Upcoming phases')
|
|
28
|
-
puts
|
|
29
|
-
|
|
30
|
-
puts '4. Current month phases:'
|
|
31
|
-
puts '-' * 25
|
|
32
|
-
current_phases = tracker.current_month_phases
|
|
33
|
-
if current_phases.any?
|
|
34
|
-
puts tracker.format_phases(current_phases, 'Current month phases')
|
|
35
|
-
else
|
|
36
|
-
puts 'No phases found for the current month.'
|
|
37
|
-
end
|
|
38
|
-
puts
|
|
39
|
-
|
|
40
|
-
puts '5. Next moon phase:'
|
|
41
|
-
puts '-' * 25
|
|
42
|
-
next_phase = tracker.next_phase
|
|
43
|
-
if next_phase
|
|
44
|
-
puts next_phase
|
|
45
|
-
puts "Details: #{next_phase.to_h}"
|
|
46
|
-
else
|
|
47
|
-
puts 'Could not retrieve the next phase.'
|
|
48
|
-
end
|
|
49
|
-
puts
|
|
50
|
-
|
|
51
|
-
puts '6. Different phase representations:'
|
|
52
|
-
puts '-' * 42
|
|
53
|
-
if august_phases.any?
|
|
54
|
-
phase = august_phases.first
|
|
55
|
-
puts "String: #{phase}"
|
|
56
|
-
puts "Hash: #{phase.to_h}"
|
|
57
|
-
puts "Symbol: #{phase.symbol}"
|
|
58
|
-
puts "Formatted date: #{phase.formatted_date}"
|
|
59
|
-
puts "Formatted time: #{phase.formatted_time}"
|
|
60
|
-
end
|
|
61
|
-
rescue MoonPhaseTracker::NetworkError => e
|
|
62
|
-
puts "Network error: #{e.message}"
|
|
63
|
-
puts 'Please check your internet connection.'
|
|
64
|
-
rescue MoonPhaseTracker::APIError => e
|
|
65
|
-
puts "API error: #{e.message}"
|
|
66
|
-
puts 'The service may be temporarily unavailable.'
|
|
67
|
-
rescue MoonPhaseTracker::InvalidDateError => e
|
|
68
|
-
puts "Date error: #{e.message}"
|
|
69
|
-
rescue StandardError => e
|
|
70
|
-
puts "Unexpected error: #{e.message}"
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
puts
|
|
74
|
-
puts '=== End of Examples ==='
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../lib/moon_phase_tracker'
|
|
5
|
+
|
|
6
|
+
puts '=== Moon Phase Tracker - Usage Examples =='
|
|
7
|
+
puts
|
|
8
|
+
|
|
9
|
+
tracker = MoonPhaseTracker::Tracker.new
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
puts '1. Moon phases for August 2025:'
|
|
13
|
+
puts '-' * 40
|
|
14
|
+
august_phases = MoonPhaseTracker.phases_for_month(2025, 8)
|
|
15
|
+
puts tracker.format_phases(august_phases, "#{MoonPhaseTracker::Tracker.month_name(8)} 2025 Phases")
|
|
16
|
+
puts
|
|
17
|
+
|
|
18
|
+
puts '2. All moon phases in 2025 (first 8):'
|
|
19
|
+
puts '-' * 50
|
|
20
|
+
year_phases = MoonPhaseTracker.phases_for_year(2025)
|
|
21
|
+
puts tracker.format_phases(year_phases.first(8), 'First 8 phases of 2025')
|
|
22
|
+
puts
|
|
23
|
+
|
|
24
|
+
puts '3. Next 6 phases starting from 2025-08-01:'
|
|
25
|
+
puts '-' * 45
|
|
26
|
+
future_phases = MoonPhaseTracker.phases_from_date('2025-08-01', 6)
|
|
27
|
+
puts tracker.format_phases(future_phases, 'Upcoming phases')
|
|
28
|
+
puts
|
|
29
|
+
|
|
30
|
+
puts '4. Current month phases:'
|
|
31
|
+
puts '-' * 25
|
|
32
|
+
current_phases = tracker.current_month_phases
|
|
33
|
+
if current_phases.any?
|
|
34
|
+
puts tracker.format_phases(current_phases, 'Current month phases')
|
|
35
|
+
else
|
|
36
|
+
puts 'No phases found for the current month.'
|
|
37
|
+
end
|
|
38
|
+
puts
|
|
39
|
+
|
|
40
|
+
puts '5. Next moon phase:'
|
|
41
|
+
puts '-' * 25
|
|
42
|
+
next_phase = tracker.next_phase
|
|
43
|
+
if next_phase
|
|
44
|
+
puts next_phase
|
|
45
|
+
puts "Details: #{next_phase.to_h}"
|
|
46
|
+
else
|
|
47
|
+
puts 'Could not retrieve the next phase.'
|
|
48
|
+
end
|
|
49
|
+
puts
|
|
50
|
+
|
|
51
|
+
puts '6. Different phase representations:'
|
|
52
|
+
puts '-' * 42
|
|
53
|
+
if august_phases.any?
|
|
54
|
+
phase = august_phases.first
|
|
55
|
+
puts "String: #{phase}"
|
|
56
|
+
puts "Hash: #{phase.to_h}"
|
|
57
|
+
puts "Symbol: #{phase.symbol}"
|
|
58
|
+
puts "Formatted date: #{phase.formatted_date}"
|
|
59
|
+
puts "Formatted time: #{phase.formatted_time}"
|
|
60
|
+
end
|
|
61
|
+
rescue MoonPhaseTracker::NetworkError => e
|
|
62
|
+
puts "Network error: #{e.message}"
|
|
63
|
+
puts 'Please check your internet connection.'
|
|
64
|
+
rescue MoonPhaseTracker::APIError => e
|
|
65
|
+
puts "API error: #{e.message}"
|
|
66
|
+
puts 'The service may be temporarily unavailable.'
|
|
67
|
+
rescue MoonPhaseTracker::InvalidDateError => e
|
|
68
|
+
puts "Date error: #{e.message}"
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
puts "Unexpected error: #{e.message}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
puts
|
|
74
|
+
puts '=== End of Examples ==='
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MoonPhaseTracker
|
|
4
|
+
class LunarCalculator
|
|
5
|
+
SYNODIC_MONTH = 29.530588853
|
|
6
|
+
KNOWN_NEW_MOON_JD = 2451550.26 # Jan 6, 2000 18:14 UTC
|
|
7
|
+
|
|
8
|
+
PHASE_BOUNDARIES = [
|
|
9
|
+
[ 0.0, "New Moon" ],
|
|
10
|
+
[ 0.0625, "Waxing Crescent" ],
|
|
11
|
+
[ 0.1875, "First Quarter" ],
|
|
12
|
+
[ 0.3125, "Waxing Gibbous" ],
|
|
13
|
+
[ 0.4375, "Full Moon" ],
|
|
14
|
+
[ 0.5625, "Waning Gibbous" ],
|
|
15
|
+
[ 0.6875, "Last Quarter" ],
|
|
16
|
+
[ 0.8125, "Waning Crescent" ]
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
def phase_at(date)
|
|
20
|
+
time = coerce_to_time(date)
|
|
21
|
+
fraction = cycle_position(time)
|
|
22
|
+
age = lunar_age(time)
|
|
23
|
+
illum = illumination_from_fraction(fraction)
|
|
24
|
+
name = classify_phase(fraction)
|
|
25
|
+
|
|
26
|
+
Phase.from_calculation(
|
|
27
|
+
name: name,
|
|
28
|
+
date: time.utc.to_date,
|
|
29
|
+
time: time.utc.strftime("%H:%M"),
|
|
30
|
+
illumination: illum,
|
|
31
|
+
lunar_age: age
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def illumination(date)
|
|
36
|
+
fraction = cycle_position(coerce_to_time(date))
|
|
37
|
+
illumination_from_fraction(fraction)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def lunar_age(date)
|
|
41
|
+
time = coerce_to_time(date)
|
|
42
|
+
jd = to_julian_date(time)
|
|
43
|
+
days_since = jd - KNOWN_NEW_MOON_JD
|
|
44
|
+
days_since % SYNODIC_MONTH
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cycle_position(date)
|
|
48
|
+
time = coerce_to_time(date)
|
|
49
|
+
jd = to_julian_date(time)
|
|
50
|
+
days_since = jd - KNOWN_NEW_MOON_JD
|
|
51
|
+
(days_since % SYNODIC_MONTH) / SYNODIC_MONTH
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def coerce_to_time(input)
|
|
57
|
+
case input
|
|
58
|
+
when Time then input.utc
|
|
59
|
+
when DateTime then input.to_time.utc
|
|
60
|
+
when Date then Time.utc(input.year, input.month, input.day, 12)
|
|
61
|
+
when String then coerce_to_time(Date.parse(input))
|
|
62
|
+
else raise ArgumentError, "Expected Date, Time, DateTime, or String. Got #{input.class}"
|
|
63
|
+
end
|
|
64
|
+
rescue Date::Error => e
|
|
65
|
+
raise ArgumentError, "Cannot parse date string: #{input.inspect} (#{e.message})"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_julian_date(time)
|
|
69
|
+
utc = time.utc
|
|
70
|
+
y = utc.year
|
|
71
|
+
m = utc.month
|
|
72
|
+
d = utc.day + (utc.hour + utc.min / 60.0 + utc.sec / 3600.0) / 24.0
|
|
73
|
+
|
|
74
|
+
if m <= 2
|
|
75
|
+
y -= 1
|
|
76
|
+
m += 12
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
a = (y / 100).floor
|
|
80
|
+
b = 2 - a + (a / 4).floor
|
|
81
|
+
|
|
82
|
+
(365.25 * (y + 4716)).floor + (30.6001 * (m + 1)).floor + d + b - 1524.5
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def illumination_from_fraction(fraction)
|
|
86
|
+
((1 - Math.cos(2 * Math::PI * fraction)) / 2.0 * 100).round(2)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def classify_phase(fraction)
|
|
90
|
+
PHASE_BOUNDARIES.reverse_each do |threshold, name|
|
|
91
|
+
return name if fraction >= threshold
|
|
92
|
+
end
|
|
93
|
+
PHASE_BOUNDARIES.first.last
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -36,7 +36,10 @@ module MoonPhaseTracker
|
|
|
36
36
|
symbol: phase_attributes[:symbol],
|
|
37
37
|
iso_date: phase_attributes[:date]&.iso8601,
|
|
38
38
|
utc_time: phase_attributes[:time]&.utc&.iso8601,
|
|
39
|
-
interpolated: phase_attributes[:interpolated]
|
|
39
|
+
interpolated: phase_attributes[:interpolated],
|
|
40
|
+
source: phase_attributes[:source],
|
|
41
|
+
illumination: phase_attributes[:illumination],
|
|
42
|
+
lunar_age: phase_attributes[:lunar_age]
|
|
40
43
|
}
|
|
41
44
|
end
|
|
42
45
|
end
|
|
@@ -18,10 +18,15 @@ module MoonPhaseTracker
|
|
|
18
18
|
nil
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def self.parse_time(time_string)
|
|
21
|
+
def self.parse_time(time_string, date = nil)
|
|
22
22
|
return nil unless time_string
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
if date
|
|
25
|
+
hour, minute = time_string.split(":").map(&:to_i)
|
|
26
|
+
Time.utc(date.year, date.month, date.day, hour, minute)
|
|
27
|
+
else
|
|
28
|
+
Time.parse("#{time_string} UTC")
|
|
29
|
+
end
|
|
25
30
|
rescue ArgumentError
|
|
26
31
|
nil
|
|
27
32
|
end
|
|
@@ -9,14 +9,29 @@ module MoonPhaseTracker
|
|
|
9
9
|
class Phase
|
|
10
10
|
include Comparable
|
|
11
11
|
|
|
12
|
-
attr_reader :name, :date, :time, :phase_type, :interpolated
|
|
12
|
+
attr_reader :name, :date, :time, :phase_type, :interpolated,
|
|
13
|
+
:source, :illumination, :lunar_age
|
|
13
14
|
|
|
14
|
-
def initialize(phase_data, interpolated: false)
|
|
15
|
+
def initialize(phase_data, interpolated: false, source: :api, illumination: nil, lunar_age: nil)
|
|
15
16
|
@name = phase_data["phase"]
|
|
16
17
|
@phase_type = Mapper.map_phase_type(@name)
|
|
17
18
|
@date = Parser.build_date(phase_data)
|
|
18
|
-
@time = Parser.parse_time(phase_data["time"])
|
|
19
|
+
@time = Parser.parse_time(phase_data["time"], @date)
|
|
19
20
|
@interpolated = interpolated
|
|
21
|
+
@source = source
|
|
22
|
+
@illumination = illumination
|
|
23
|
+
@lunar_age = lunar_age
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.from_calculation(name:, date:, time:, illumination:, lunar_age:)
|
|
27
|
+
phase_data = {
|
|
28
|
+
"phase" => name,
|
|
29
|
+
"year" => date.year,
|
|
30
|
+
"month" => date.month,
|
|
31
|
+
"day" => date.day,
|
|
32
|
+
"time" => time
|
|
33
|
+
}
|
|
34
|
+
new(phase_data, source: :calculated, illumination: illumination, lunar_age: lunar_age)
|
|
20
35
|
end
|
|
21
36
|
|
|
22
37
|
def formatted_date
|
|
@@ -42,7 +57,10 @@ module MoonPhaseTracker
|
|
|
42
57
|
date: @date,
|
|
43
58
|
time: @time,
|
|
44
59
|
symbol: symbol,
|
|
45
|
-
interpolated: @interpolated
|
|
60
|
+
interpolated: @interpolated,
|
|
61
|
+
source: @source,
|
|
62
|
+
illumination: @illumination,
|
|
63
|
+
lunar_age: @lunar_age
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
Formatter.build_hash_representation(phase_attributes)
|
|
@@ -15,7 +15,7 @@ module MoonPhaseTracker
|
|
|
15
15
|
estimated_date = calculate_estimated_date(last_phase)
|
|
16
16
|
phase_data = build_estimated_phase_data(estimated_date, last_phase)
|
|
17
17
|
|
|
18
|
-
Phase.new(phase_data)
|
|
18
|
+
Phase.new(phase_data, source: :interpolated)
|
|
19
19
|
rescue Date::Error, ArgumentError => e
|
|
20
20
|
warn "Failed to estimate next cycle phase: #{e.class}"
|
|
21
21
|
nil
|
|
@@ -60,7 +60,7 @@ module MoonPhaseTracker
|
|
|
60
60
|
return nil unless intermediate_datetime
|
|
61
61
|
|
|
62
62
|
phase_data = build_phase_data(intermediate_datetime, config[:name])
|
|
63
|
-
Phase.new(phase_data, interpolated: true)
|
|
63
|
+
Phase.new(phase_data, interpolated: true, source: :interpolated)
|
|
64
64
|
rescue Date::Error, ArgumentError => e
|
|
65
65
|
warn "Failed to create intermediate phase: #{e.class}"
|
|
66
66
|
nil
|
|
@@ -43,6 +43,20 @@ module MoonPhaseTracker
|
|
|
43
43
|
phases_from_date(Date.today, 1).first
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
def phase_at(date)
|
|
47
|
+
parsed = @date_parser.parse(date)
|
|
48
|
+
calculator.phase_at(parsed)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def illumination(date)
|
|
52
|
+
parsed = @date_parser.parse(date)
|
|
53
|
+
calculator.illumination(parsed)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def current_phase
|
|
57
|
+
calculator.phase_at(Time.now.utc)
|
|
58
|
+
end
|
|
59
|
+
|
|
46
60
|
def current_month_phases
|
|
47
61
|
today = Date.today
|
|
48
62
|
phases_for_month(today.year, today.month)
|
|
@@ -77,6 +91,10 @@ module MoonPhaseTracker
|
|
|
77
91
|
|
|
78
92
|
private
|
|
79
93
|
|
|
94
|
+
def calculator
|
|
95
|
+
@calculator ||= LunarCalculator.new
|
|
96
|
+
end
|
|
97
|
+
|
|
80
98
|
MONTH_NAMES = %w[
|
|
81
99
|
January February March April May June
|
|
82
100
|
July August September October November December
|
data/lib/moon_phase_tracker.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "moon_phase_tracker/rate_limiter"
|
|
|
5
5
|
require_relative "moon_phase_tracker/client"
|
|
6
6
|
require_relative "moon_phase_tracker/phase"
|
|
7
7
|
require_relative "moon_phase_tracker/phase_calculator"
|
|
8
|
+
require_relative "moon_phase_tracker/lunar_calculator"
|
|
8
9
|
require_relative "moon_phase_tracker/tracker"
|
|
9
10
|
|
|
10
11
|
module MoonPhaseTracker
|
|
@@ -39,4 +40,20 @@ module MoonPhaseTracker
|
|
|
39
40
|
def self.all_phases_from_date(date, num_cycles = 3)
|
|
40
41
|
Tracker.new.all_phases_from_date(date, num_cycles)
|
|
41
42
|
end
|
|
43
|
+
|
|
44
|
+
def self.calculator
|
|
45
|
+
@calculator ||= LunarCalculator.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.phase_at(date)
|
|
49
|
+
calculator.phase_at(date)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.illumination(date)
|
|
53
|
+
calculator.illumination(date)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.current_phase
|
|
57
|
+
calculator.phase_at(Time.now.utc)
|
|
58
|
+
end
|
|
42
59
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: moon_phase_tracker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel K Lima
|
|
@@ -56,6 +56,7 @@ files:
|
|
|
56
56
|
- examples/usage_example.rb
|
|
57
57
|
- lib/moon_phase_tracker.rb
|
|
58
58
|
- lib/moon_phase_tracker/client.rb
|
|
59
|
+
- lib/moon_phase_tracker/lunar_calculator.rb
|
|
59
60
|
- lib/moon_phase_tracker/phase.rb
|
|
60
61
|
- lib/moon_phase_tracker/phase/comparator.rb
|
|
61
62
|
- lib/moon_phase_tracker/phase/formatter.rb
|