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.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MoonPhaseTracker
4
+ VERSION = "1.3.2"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module MoonPhaseTracker
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ 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: []