moon_phase_tracker 1.3.2
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 +7 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +166 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +302 -0
- data/Rakefile +8 -0
- data/examples/eight_phases_example.rb +100 -0
- data/examples/rate_limiting_example.rb +117 -0
- data/examples/usage_example.rb +74 -0
- data/lib/moon_phase_tracker/client.rb +116 -0
- data/lib/moon_phase_tracker/phase/comparator.rb +42 -0
- data/lib/moon_phase_tracker/phase/formatter.rb +44 -0
- data/lib/moon_phase_tracker/phase/mapper.rb +56 -0
- data/lib/moon_phase_tracker/phase/parser.rb +36 -0
- data/lib/moon_phase_tracker/phase.rb +63 -0
- data/lib/moon_phase_tracker/phase_calculator/cycle_estimator.rb +51 -0
- data/lib/moon_phase_tracker/phase_calculator/phase_interpolator.rb +113 -0
- data/lib/moon_phase_tracker/phase_calculator.rb +45 -0
- data/lib/moon_phase_tracker/rate_limiter.rb +102 -0
- data/lib/moon_phase_tracker/tracker/date_parser.rb +32 -0
- data/lib/moon_phase_tracker/tracker/phase_formatter.rb +64 -0
- data/lib/moon_phase_tracker/tracker/phase_query_service.rb +71 -0
- data/lib/moon_phase_tracker/tracker/validators.rb +32 -0
- data/lib/moon_phase_tracker/tracker.rb +85 -0
- data/lib/moon_phase_tracker/version.rb +5 -0
- data/lib/moon_phase_tracker.rb +42 -0
- data/sig/moon_phase_tracker.rbs +4 -0
- metadata +100 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../phase"
|
4
|
+
|
5
|
+
module MoonPhaseTracker
|
6
|
+
class PhaseCalculator
|
7
|
+
class PhaseInterpolator
|
8
|
+
MAX_INTERPOLATION_INTERVAL = 11.07
|
9
|
+
HOURS_PER_DAY = 24
|
10
|
+
SECONDS_PER_HOUR = 3600
|
11
|
+
DEFAULT_HOUR = 12
|
12
|
+
INTERMEDIATE_PHASES = [
|
13
|
+
{ name: "Waxing Crescent", offset_ratio: 0.5, between: %i[new_moon first_quarter] },
|
14
|
+
{ name: "Waxing Gibbous", offset_ratio: 0.5, between: %i[first_quarter full_moon] },
|
15
|
+
{ name: "Waning Gibbous", offset_ratio: 0.5, between: %i[full_moon last_quarter] },
|
16
|
+
{ name: "Waning Crescent", offset_ratio: 0.5, between: %i[last_quarter new_moon] }
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
def calculate_between(phase1, phase2)
|
20
|
+
return nil unless interpolation_possible?(phase1, phase2)
|
21
|
+
|
22
|
+
intermediate_config = find_intermediate_config(phase1.phase_type, phase2.phase_type)
|
23
|
+
return nil unless intermediate_config
|
24
|
+
|
25
|
+
create_intermediate_phase(phase1, phase2, intermediate_config)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def interpolation_possible?(phase1, phase2)
|
31
|
+
return false unless valid_phases?(phase1, phase2)
|
32
|
+
return false unless valid_dates?(phase1, phase2)
|
33
|
+
|
34
|
+
interval_within_limits?(phase1, phase2)
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid_phases?(phase1, phase2)
|
38
|
+
phase1 && phase2
|
39
|
+
end
|
40
|
+
|
41
|
+
def valid_dates?(phase1, phase2)
|
42
|
+
phase1.date && phase2.date
|
43
|
+
end
|
44
|
+
|
45
|
+
def interval_within_limits?(phase1, phase2)
|
46
|
+
days_between = calculate_days_between(phase1.date, phase2.date)
|
47
|
+
days_between.positive? && days_between <= MAX_INTERPOLATION_INTERVAL
|
48
|
+
end
|
49
|
+
|
50
|
+
def calculate_days_between(date1, date2)
|
51
|
+
(date2 - date1).to_f
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_intermediate_config(type1, type2)
|
55
|
+
INTERMEDIATE_PHASES.find { |config| config[:between] == [ type1, type2 ] }
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_intermediate_phase(phase1, phase2, config)
|
59
|
+
intermediate_datetime = calculate_intermediate_datetime(phase1, phase2, config)
|
60
|
+
return nil unless intermediate_datetime
|
61
|
+
|
62
|
+
phase_data = build_phase_data(intermediate_datetime, config[:name])
|
63
|
+
Phase.new(phase_data, interpolated: true)
|
64
|
+
rescue Date::Error, ArgumentError => e
|
65
|
+
warn "Failed to create intermediate phase: #{e.class}"
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def calculate_intermediate_datetime(phase1, phase2, config)
|
70
|
+
total_hours = calculate_total_hours_between(phase1, phase2)
|
71
|
+
intermediate_hours = total_hours * config[:offset_ratio]
|
72
|
+
|
73
|
+
base_time = determine_base_time(phase1)
|
74
|
+
base_time + (intermediate_hours * SECONDS_PER_HOUR)
|
75
|
+
end
|
76
|
+
|
77
|
+
def calculate_total_hours_between(phase1, phase2)
|
78
|
+
days_between = calculate_days_between(phase1.date, phase2.date)
|
79
|
+
hours_from_days = days_between * HOURS_PER_DAY
|
80
|
+
|
81
|
+
return hours_from_days unless both_phases_have_time?(phase1, phase2)
|
82
|
+
|
83
|
+
time_difference_hours = calculate_time_difference_hours(phase1.time, phase2.time)
|
84
|
+
hours_from_days + time_difference_hours
|
85
|
+
end
|
86
|
+
|
87
|
+
def both_phases_have_time?(phase1, phase2)
|
88
|
+
phase1.time && phase2.time
|
89
|
+
end
|
90
|
+
|
91
|
+
def calculate_time_difference_hours(time1, time2)
|
92
|
+
(time2 - time1) / SECONDS_PER_HOUR.to_f
|
93
|
+
end
|
94
|
+
|
95
|
+
def determine_base_time(phase)
|
96
|
+
return phase.time if phase.time
|
97
|
+
|
98
|
+
Time.new(phase.date.year, phase.date.month, phase.date.day,
|
99
|
+
DEFAULT_HOUR, 0, 0, "+00:00")
|
100
|
+
end
|
101
|
+
|
102
|
+
def build_phase_data(datetime, name)
|
103
|
+
{
|
104
|
+
"phase" => name,
|
105
|
+
"year" => datetime.year,
|
106
|
+
"month" => datetime.month,
|
107
|
+
"day" => datetime.day,
|
108
|
+
"time" => datetime.strftime("%H:%M")
|
109
|
+
}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require_relative "phase_calculator/phase_interpolator"
|
5
|
+
require_relative "phase_calculator/cycle_estimator"
|
6
|
+
|
7
|
+
module MoonPhaseTracker
|
8
|
+
class PhaseCalculator
|
9
|
+
def initialize(major_phases)
|
10
|
+
@major_phases = major_phases.sort
|
11
|
+
@interpolator = PhaseInterpolator.new
|
12
|
+
@cycle_estimator = CycleEstimator.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def calculate_all_phases
|
16
|
+
all_phases = @major_phases.dup
|
17
|
+
|
18
|
+
add_consecutive_intermediate_phases(all_phases)
|
19
|
+
add_cycle_transition_phases(all_phases)
|
20
|
+
|
21
|
+
all_phases.sort
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def add_consecutive_intermediate_phases(all_phases)
|
27
|
+
@major_phases.each_cons(2) do |phase1, phase2|
|
28
|
+
intermediate_phase = @interpolator.calculate_between(phase1, phase2)
|
29
|
+
all_phases << intermediate_phase if intermediate_phase
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_cycle_transition_phases(all_phases)
|
34
|
+
return unless @major_phases.size >= 2
|
35
|
+
|
36
|
+
last_phase = @major_phases.last
|
37
|
+
next_cycle_phase = @cycle_estimator.estimate_next_cycle_phase(last_phase)
|
38
|
+
|
39
|
+
return unless next_cycle_phase
|
40
|
+
|
41
|
+
intermediate_phase = @interpolator.calculate_between(last_phase, next_cycle_phase)
|
42
|
+
all_phases << intermediate_phase if intermediate_phase
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thread"
|
4
|
+
|
5
|
+
module MoonPhaseTracker
|
6
|
+
class RateLimiter
|
7
|
+
DEFAULT_REQUESTS_PER_SECOND = 1.0
|
8
|
+
DEFAULT_BURST_SIZE = 1
|
9
|
+
|
10
|
+
def initialize(requests_per_second: nil, burst_size: nil)
|
11
|
+
@requests_per_second = parse_rate_limit(requests_per_second)
|
12
|
+
@burst_size = parse_burst_size(burst_size)
|
13
|
+
@tokens = @burst_size.to_f
|
14
|
+
@last_refill = current_time
|
15
|
+
@mutex = Mutex.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def throttle
|
19
|
+
@mutex.synchronize do
|
20
|
+
refill_tokens
|
21
|
+
|
22
|
+
if @tokens >= 1.0
|
23
|
+
consume_token
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
wait_time = calculate_wait_time
|
28
|
+
sleep(wait_time) if wait_time > 0
|
29
|
+
|
30
|
+
refill_tokens
|
31
|
+
consume_token
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def can_proceed?
|
36
|
+
@mutex.synchronize do
|
37
|
+
refill_tokens
|
38
|
+
@tokens >= 1.0
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def configuration
|
43
|
+
{
|
44
|
+
requests_per_second: @requests_per_second,
|
45
|
+
burst_size: @burst_size,
|
46
|
+
available_tokens: @tokens.floor
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def parse_rate_limit(rate)
|
53
|
+
rate = rate || ENV.fetch("MOON_PHASE_RATE_LIMIT", DEFAULT_REQUESTS_PER_SECOND)
|
54
|
+
|
55
|
+
case rate
|
56
|
+
when String
|
57
|
+
Float(rate)
|
58
|
+
when Numeric
|
59
|
+
rate.to_f
|
60
|
+
else
|
61
|
+
raise ArgumentError, "Rate limit must be a number"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_burst_size(size)
|
66
|
+
size = size || ENV.fetch("MOON_PHASE_BURST_SIZE", DEFAULT_BURST_SIZE)
|
67
|
+
|
68
|
+
case size
|
69
|
+
when String
|
70
|
+
Integer(size)
|
71
|
+
when Numeric
|
72
|
+
size.to_i
|
73
|
+
else
|
74
|
+
raise ArgumentError, "Burst size must be an integer"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def refill_tokens
|
79
|
+
now = current_time
|
80
|
+
elapsed = now - @last_refill
|
81
|
+
tokens_to_add = elapsed * @requests_per_second
|
82
|
+
|
83
|
+
@tokens = [ @tokens + tokens_to_add, @burst_size ].min
|
84
|
+
@last_refill = now
|
85
|
+
end
|
86
|
+
|
87
|
+
def calculate_wait_time
|
88
|
+
return 0 if @tokens >= 1.0
|
89
|
+
|
90
|
+
tokens_needed = 1.0 - @tokens
|
91
|
+
tokens_needed / @requests_per_second
|
92
|
+
end
|
93
|
+
|
94
|
+
def consume_token
|
95
|
+
@tokens = [ @tokens - 1.0, 0.0 ].max
|
96
|
+
end
|
97
|
+
|
98
|
+
def current_time
|
99
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
|
5
|
+
module MoonPhaseTracker
|
6
|
+
class DateParser
|
7
|
+
def parse(date)
|
8
|
+
return parse_by_type(date) if valid_date_type?(date)
|
9
|
+
|
10
|
+
raise InvalidDateError, "Invalid date format: #{date.class}"
|
11
|
+
rescue Date::Error => e
|
12
|
+
raise InvalidDateError, "Invalid date: #{date} (#{e.message})"
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def valid_date_type?(date)
|
18
|
+
[ String, Date, Time ].any? { |klass| date.is_a?(klass) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_by_type(date)
|
22
|
+
case date
|
23
|
+
when String
|
24
|
+
Date.parse(date)
|
25
|
+
when Date
|
26
|
+
date
|
27
|
+
when Time
|
28
|
+
date.to_date
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MoonPhaseTracker
|
4
|
+
class PhaseFormatter
|
5
|
+
def format(phases, title = nil)
|
6
|
+
return "No phases found." if phases.empty?
|
7
|
+
|
8
|
+
build_formatted_output(phases, title)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def build_formatted_output(phases, title)
|
14
|
+
output_lines = []
|
15
|
+
|
16
|
+
add_title_section(output_lines, title)
|
17
|
+
add_phases_section(output_lines, phases)
|
18
|
+
add_statistics_section(output_lines, phases)
|
19
|
+
|
20
|
+
output_lines.join("\n")
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_title_section(output_lines, title)
|
24
|
+
return unless title
|
25
|
+
|
26
|
+
output_lines << title
|
27
|
+
output_lines << "=" * title.length
|
28
|
+
output_lines << ""
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_phases_section(output_lines, phases)
|
32
|
+
phases.each do |phase|
|
33
|
+
prefix = phase.interpolated ? "~" : " "
|
34
|
+
output_lines << "#{prefix}#{phase}"
|
35
|
+
end
|
36
|
+
|
37
|
+
output_lines << ""
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_statistics_section(output_lines, phases)
|
41
|
+
phase_stats = calculate_phase_statistics(phases)
|
42
|
+
|
43
|
+
if phase_stats[:interpolated_count].positive?
|
44
|
+
add_detailed_statistics(output_lines, phase_stats)
|
45
|
+
output_lines << "~ indicates interpolated phases"
|
46
|
+
else
|
47
|
+
output_lines << "Total: #{phase_stats[:total_count]} phase(s)"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def calculate_phase_statistics(phases)
|
52
|
+
{
|
53
|
+
total_count: phases.size,
|
54
|
+
major_count: phases.count { |phase| !phase.interpolated },
|
55
|
+
interpolated_count: phases.count(&:interpolated)
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_detailed_statistics(output_lines, stats)
|
60
|
+
output_lines << "Total: #{stats[:total_count]} phase(s) " \
|
61
|
+
"(#{stats[:major_count]} major, #{stats[:interpolated_count]} interpolated)"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MoonPhaseTracker
|
4
|
+
class PhaseQueryService
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def initialize(client)
|
8
|
+
@client = client
|
9
|
+
end
|
10
|
+
|
11
|
+
def phases_for_month(year, month)
|
12
|
+
year_phases = phases_for_year(year)
|
13
|
+
filter_phases_by_month(year_phases, year, month)
|
14
|
+
end
|
15
|
+
|
16
|
+
def phases_for_year(year)
|
17
|
+
response = @client.phases_for_year(year)
|
18
|
+
parse_api_response(response)
|
19
|
+
end
|
20
|
+
|
21
|
+
def phases_from_date(date, num_phases)
|
22
|
+
formatted_date = format_date_for_api(date)
|
23
|
+
response = @client.phases_from_date(formatted_date, num_phases)
|
24
|
+
parse_api_response(response)
|
25
|
+
end
|
26
|
+
|
27
|
+
def all_phases_for_month(year, month)
|
28
|
+
major_phases = phases_for_month(year, month)
|
29
|
+
all_phases = calculate_all_phases(major_phases)
|
30
|
+
filter_phases_by_month(all_phases, year, month)
|
31
|
+
end
|
32
|
+
|
33
|
+
def all_phases_for_year(year)
|
34
|
+
major_phases = phases_for_year(year)
|
35
|
+
calculate_all_phases(major_phases)
|
36
|
+
end
|
37
|
+
|
38
|
+
def all_phases_from_date(date, num_cycles)
|
39
|
+
# Calculate enough major phases to cover the requested cycles
|
40
|
+
phases_needed = num_cycles * 4
|
41
|
+
major_phases = phases_from_date(date, phases_needed)
|
42
|
+
calculate_all_phases(major_phases)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def parse_api_response(response)
|
48
|
+
return [] unless valid_response?(response)
|
49
|
+
|
50
|
+
phases = response["phasedata"].map { |phase_data| Phase.new(phase_data) }
|
51
|
+
phases.sort
|
52
|
+
end
|
53
|
+
|
54
|
+
def valid_response?(response)
|
55
|
+
response && response["phasedata"]
|
56
|
+
end
|
57
|
+
|
58
|
+
def filter_phases_by_month(phases, year, month)
|
59
|
+
phases.select { |phase| phase.in_month?(year, month) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def format_date_for_api(date)
|
63
|
+
date.strftime("%Y-%m-%d")
|
64
|
+
end
|
65
|
+
|
66
|
+
def calculate_all_phases(major_phases)
|
67
|
+
calculator = PhaseCalculator.new(major_phases)
|
68
|
+
calculator.calculate_all_phases
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MoonPhaseTracker
|
4
|
+
module Validators
|
5
|
+
private
|
6
|
+
|
7
|
+
def validate_year!(year)
|
8
|
+
current_year = Date.today.year
|
9
|
+
valid_range = (1700..current_year + 10)
|
10
|
+
|
11
|
+
return if year.is_a?(Integer) && valid_range.cover?(year)
|
12
|
+
|
13
|
+
raise InvalidDateError, "Year must be between #{valid_range.min} and #{valid_range.max}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate_month!(year, month)
|
17
|
+
validate_year!(year)
|
18
|
+
|
19
|
+
return if month.is_a?(Integer) && (1..12).cover?(month)
|
20
|
+
|
21
|
+
raise InvalidDateError, "Month must be between 1 and 12"
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate_num_phases!(num_phases)
|
25
|
+
valid_range = (1..99)
|
26
|
+
|
27
|
+
return if num_phases.is_a?(Integer) && valid_range.cover?(num_phases)
|
28
|
+
|
29
|
+
raise InvalidDateError, "Number of phases must be between #{valid_range.min} and #{valid_range.max}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require_relative "tracker/phase_query_service"
|
5
|
+
require_relative "tracker/phase_formatter"
|
6
|
+
require_relative "tracker/date_parser"
|
7
|
+
require_relative "tracker/validators"
|
8
|
+
|
9
|
+
module MoonPhaseTracker
|
10
|
+
class Tracker
|
11
|
+
include Validators
|
12
|
+
|
13
|
+
attr_reader :client, :query_service, :formatter, :date_parser
|
14
|
+
|
15
|
+
def initialize(rate_limiter: nil)
|
16
|
+
@client = Client.new(rate_limiter: rate_limiter)
|
17
|
+
@query_service = PhaseQueryService.new(@client)
|
18
|
+
@formatter = PhaseFormatter.new
|
19
|
+
@date_parser = DateParser.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def rate_limit_info
|
23
|
+
@client.rate_limit_info
|
24
|
+
end
|
25
|
+
|
26
|
+
def phases_for_month(year, month)
|
27
|
+
validate_month!(year, month)
|
28
|
+
@query_service.phases_for_month(year, month)
|
29
|
+
end
|
30
|
+
|
31
|
+
def phases_for_year(year)
|
32
|
+
validate_year!(year)
|
33
|
+
@query_service.phases_for_year(year)
|
34
|
+
end
|
35
|
+
|
36
|
+
def phases_from_date(date, num_phases = 12)
|
37
|
+
parsed_date = @date_parser.parse(date)
|
38
|
+
validate_num_phases!(num_phases)
|
39
|
+
@query_service.phases_from_date(parsed_date, num_phases)
|
40
|
+
end
|
41
|
+
|
42
|
+
def next_phase
|
43
|
+
phases_from_date(Date.today, 1).first
|
44
|
+
end
|
45
|
+
|
46
|
+
def current_month_phases
|
47
|
+
today = Date.today
|
48
|
+
phases_for_month(today.year, today.month)
|
49
|
+
end
|
50
|
+
|
51
|
+
def current_year_phases
|
52
|
+
phases_for_year(Date.today.year)
|
53
|
+
end
|
54
|
+
|
55
|
+
def all_phases_for_month(year, month)
|
56
|
+
validate_month!(year, month)
|
57
|
+
@query_service.all_phases_for_month(year, month)
|
58
|
+
end
|
59
|
+
|
60
|
+
def all_phases_for_year(year)
|
61
|
+
validate_year!(year)
|
62
|
+
@query_service.all_phases_for_year(year)
|
63
|
+
end
|
64
|
+
|
65
|
+
def all_phases_from_date(date, num_cycles = 3)
|
66
|
+
parsed_date = @date_parser.parse(date)
|
67
|
+
@query_service.all_phases_from_date(parsed_date, num_cycles)
|
68
|
+
end
|
69
|
+
|
70
|
+
def format_phases(phases, title = nil)
|
71
|
+
@formatter.format(phases, title)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.month_name(month)
|
75
|
+
MONTH_NAMES[month - 1]
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
MONTH_NAMES = %w[
|
81
|
+
January February March April May June
|
82
|
+
July August September October November December
|
83
|
+
].freeze
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "moon_phase_tracker/version"
|
4
|
+
require_relative "moon_phase_tracker/rate_limiter"
|
5
|
+
require_relative "moon_phase_tracker/client"
|
6
|
+
require_relative "moon_phase_tracker/phase"
|
7
|
+
require_relative "moon_phase_tracker/phase_calculator"
|
8
|
+
require_relative "moon_phase_tracker/tracker"
|
9
|
+
|
10
|
+
module MoonPhaseTracker
|
11
|
+
class Error < StandardError; end
|
12
|
+
class APIError < Error; end
|
13
|
+
class NetworkError < Error; end
|
14
|
+
class InvalidDateError < Error; end
|
15
|
+
|
16
|
+
def self.phases_for_month(year, month)
|
17
|
+
Tracker.new.phases_for_month(year, month)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.phases_for_year(year)
|
21
|
+
Tracker.new.phases_for_year(year)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.phases_from_date(date, num_phases = 12)
|
25
|
+
Tracker.new.phases_from_date(date, num_phases)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get all 8 phases (4 major + 4 intermediate) for a month
|
29
|
+
def self.all_phases_for_month(year, month)
|
30
|
+
Tracker.new.all_phases_for_month(year, month)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get all 8 phases (4 major + 4 intermediate) for a year
|
34
|
+
def self.all_phases_for_year(year)
|
35
|
+
Tracker.new.all_phases_for_year(year)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get all 8 phases from a specific date
|
39
|
+
def self.all_phases_from_date(date, num_cycles = 3)
|
40
|
+
Tracker.new.all_phases_from_date(date, num_cycles)
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: moon_phase_tracker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.3.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel K Lima
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: json
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '2.7'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '2.7'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: net-http
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0.4'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.4'
|
40
|
+
description: A Ruby gem to track moon phases using the US Naval Observatory API. Shows
|
41
|
+
moon phases for specific dates, months, or years for lunar calendar scheduling.
|
42
|
+
email:
|
43
|
+
- dklima@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".rubocop.yml"
|
49
|
+
- CHANGELOG.md
|
50
|
+
- CODE_OF_CONDUCT.md
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- examples/eight_phases_example.rb
|
55
|
+
- examples/rate_limiting_example.rb
|
56
|
+
- examples/usage_example.rb
|
57
|
+
- lib/moon_phase_tracker.rb
|
58
|
+
- lib/moon_phase_tracker/client.rb
|
59
|
+
- lib/moon_phase_tracker/phase.rb
|
60
|
+
- lib/moon_phase_tracker/phase/comparator.rb
|
61
|
+
- lib/moon_phase_tracker/phase/formatter.rb
|
62
|
+
- lib/moon_phase_tracker/phase/mapper.rb
|
63
|
+
- lib/moon_phase_tracker/phase/parser.rb
|
64
|
+
- lib/moon_phase_tracker/phase_calculator.rb
|
65
|
+
- lib/moon_phase_tracker/phase_calculator/cycle_estimator.rb
|
66
|
+
- lib/moon_phase_tracker/phase_calculator/phase_interpolator.rb
|
67
|
+
- lib/moon_phase_tracker/rate_limiter.rb
|
68
|
+
- lib/moon_phase_tracker/tracker.rb
|
69
|
+
- lib/moon_phase_tracker/tracker/date_parser.rb
|
70
|
+
- lib/moon_phase_tracker/tracker/phase_formatter.rb
|
71
|
+
- lib/moon_phase_tracker/tracker/phase_query_service.rb
|
72
|
+
- lib/moon_phase_tracker/tracker/validators.rb
|
73
|
+
- lib/moon_phase_tracker/version.rb
|
74
|
+
- sig/moon_phase_tracker.rbs
|
75
|
+
homepage: https://github.com/dklima/moon_phase_tracker
|
76
|
+
licenses:
|
77
|
+
- MIT
|
78
|
+
metadata:
|
79
|
+
allowed_push_host: https://rubygems.org
|
80
|
+
source_code_uri: https://github.com/dklima/moon_phase_tracker
|
81
|
+
changelog_uri: https://github.com/dklima/moon_phase_tracker/blob/main/CHANGELOG.md
|
82
|
+
documentation_uri: https://github.com/dklima/moon_phase_tracker/blob/main/README.md
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 3.2.0
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
requirements: []
|
97
|
+
rubygems_version: 3.6.7
|
98
|
+
specification_version: 4
|
99
|
+
summary: Moon phase tracker using USNO Navy API
|
100
|
+
test_files: []
|