astroscript 0.3.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 +7 -0
- data/.rubocop.defaults.yml +5818 -0
- data/.rubocop.yml +25 -0
- data/.ruby-version +1 -0
- data/ARCH.md +13 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/Rakefile +12 -0
- data/exe/astroscript +4 -0
- data/lib/astro_helper.rb +360 -0
- data/lib/astroscript/aspect.rb +188 -0
- data/lib/astroscript/body/const_body.rb +55 -0
- data/lib/astroscript/body/house_cusp.rb +34 -0
- data/lib/astroscript/body/method_body.rb +27 -0
- data/lib/astroscript/body/midpoint.rb +47 -0
- data/lib/astroscript/body.rb +495 -0
- data/lib/astroscript/calculator.rb +257 -0
- data/lib/astroscript/chart.rb +239 -0
- data/lib/astroscript/version.rb +5 -0
- data/lib/astroscript.rb +39 -0
- data/lib/ruby_extensions.rb +83 -0
- data/lib/swiss_ephemeris.rb +106 -0
- data/sig/astroscript.rbs +4 -0
- data/vendor/swe_data/houses.txt +33 -0
- data/vendor/swe_data/s136199s.se1 +0 -0
- data/vendor/swe_data/se00010s.se1 +0 -0
- data/vendor/swe_data/se90377s.se1 +0 -0
- data/vendor/swe_data/se90482s.se1 +0 -0
- data/vendor/swe_data/seas_18.se1 +0 -0
- data/vendor/swe_data/semo_18.se1 +0 -0
- data/vendor/swe_data/sepl_18.se1 +0 -0
- metadata +318 -0
data/.rubocop.yml
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# The behavior of RuboCop can be controlled via the .rubocop.yml
|
2
|
+
# configuration file. It makes it possible to enable/disable
|
3
|
+
# certain cops (checks) and to alter their behavior if they accept
|
4
|
+
# any parameters. The file can be placed either in your home
|
5
|
+
# directory or in some project directory.
|
6
|
+
#
|
7
|
+
# RuboCop will start looking for the configuration file in the directory
|
8
|
+
# where the inspected file is and continue its way up to the root directory.
|
9
|
+
#
|
10
|
+
# See https://docs.rubocop.org/rubocop/configuration
|
11
|
+
|
12
|
+
AllCops:
|
13
|
+
NewCops: enable
|
14
|
+
|
15
|
+
Layout/SpaceBeforeBlockBraces:
|
16
|
+
EnforcedStyle: no_space
|
17
|
+
|
18
|
+
Layout/SpaceInsideBlockBraces:
|
19
|
+
SpaceBeforeBlockParameters: false
|
20
|
+
|
21
|
+
Style/StringLiterals:
|
22
|
+
EnforcedStyle: double_quotes
|
23
|
+
|
24
|
+
Style/CommentedKeyword:
|
25
|
+
Enabled: false
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.3.4
|
data/ARCH.md
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 David Löwenfels
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
data/exe/astroscript
ADDED
data/lib/astro_helper.rb
ADDED
@@ -0,0 +1,360 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "httparty"
|
4
|
+
require "geocoder"
|
5
|
+
require "tzinfo"
|
6
|
+
require "json"
|
7
|
+
|
8
|
+
module AstroHelper
|
9
|
+
module_function
|
10
|
+
|
11
|
+
PLANETS = %i[SO MO ME VE MA JU SA UR NE PL].freeze
|
12
|
+
EXTRAS = %i[AC MC NN CH VX].freeze
|
13
|
+
URANIAN = %i[CU HA ZE KR AN AD VU PO].freeze
|
14
|
+
ASTEROIDS = %i[CE PA JU VA].freeze
|
15
|
+
LOTS = %i[POF POS POH].freeze
|
16
|
+
|
17
|
+
LUNAR_PHASES = %i[new crescent first_quarter gibbous full disseminating last_quarter balsamic].freeze
|
18
|
+
PHASE_GLYPHS = %w[🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘].freeze # northern hemisphere
|
19
|
+
|
20
|
+
def print_symbol(x)
|
21
|
+
mp = x.to_s.split("/")
|
22
|
+
if mp.size > 1
|
23
|
+
mp.each{|p| print_symbol(p) }.join("/")
|
24
|
+
else
|
25
|
+
SwissEphemeris::BODIES[x.to_sym][:symbol]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def symbolize(s)
|
30
|
+
s.to_s.delete(" ").sub(".", "_").downcase.to_sym
|
31
|
+
end
|
32
|
+
|
33
|
+
def titleize(str)
|
34
|
+
str = str.to_s
|
35
|
+
str.split("_").map(&:capitalize).join(" ").gsub("Of", "of")
|
36
|
+
end
|
37
|
+
|
38
|
+
def init_calc(calc, *args)
|
39
|
+
case args.size
|
40
|
+
when 1
|
41
|
+
ary = args.first.dup
|
42
|
+
tz = ary.shift
|
43
|
+
tz = tz["name"] if tz.respond_to?(:[])
|
44
|
+
tz = tz.name if tz.respond_to?(:name)
|
45
|
+
calc.tz = tz
|
46
|
+
time = ary.shift
|
47
|
+
if time.respond_to?(:jd)
|
48
|
+
time = time.jd
|
49
|
+
else
|
50
|
+
time = datetime_to_jd(time) unless time.is_a?(Numeric)
|
51
|
+
end
|
52
|
+
calc.jd = time
|
53
|
+
calc.set_topo(*ary[0...3])
|
54
|
+
calc.datetime = DateTime.now if calc.jd.zero?
|
55
|
+
calc
|
56
|
+
else
|
57
|
+
year, month, day, hour, minute, location, name = *args
|
58
|
+
second = ((minute - minute.floor) * 60).round
|
59
|
+
minute = minute.floor
|
60
|
+
results = Geocoder.search(location)
|
61
|
+
calc.set_topo(results.first.latitude, results.first.longitude)
|
62
|
+
tz = results.first.send(:properties)["timezone"]
|
63
|
+
calc.tz = tz["name"]
|
64
|
+
calc.datetime = TZInfo::Timezone.get(tz["name"]).local_time(year, month, day, hour, minute, second, 10/600r).utc
|
65
|
+
calc.to_a + [location, name]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def dms(degree, zodiac = true)
|
70
|
+
d = degree.abs.floor
|
71
|
+
m = (degree.abs - d) * 60.0
|
72
|
+
if m.round == 60
|
73
|
+
m = 0
|
74
|
+
d += 1
|
75
|
+
end
|
76
|
+
s = (m - m.floor) * 60.0
|
77
|
+
if s.round == 60
|
78
|
+
s = 0
|
79
|
+
m += 1
|
80
|
+
end
|
81
|
+
d %= 30 if zodiac
|
82
|
+
[d, m, s]
|
83
|
+
end
|
84
|
+
|
85
|
+
def dms_to_deg(d, m, s)
|
86
|
+
d + (m * 60) + (s * 3600)
|
87
|
+
end
|
88
|
+
|
89
|
+
def print_dms(deg, opts = {})
|
90
|
+
if opts[:flavor] == :astrolog
|
91
|
+
opts[:zodiac] = true
|
92
|
+
opts[:glyph] = false
|
93
|
+
opts[:minutes] = ""
|
94
|
+
end
|
95
|
+
opts[:glyph] = true if opts[:glyph].nil?
|
96
|
+
d = deg.to_f
|
97
|
+
neg = d.negative?
|
98
|
+
d, m, s = dms(d, false)
|
99
|
+
if opts[:zodiac]
|
100
|
+
sign, d = d.divmod(30)
|
101
|
+
opts[:separator] = opts[:glyph] ? SwissEphemeris::ZODIAC_SYM[sign] : SwissEphemeris::SIGNS[sign][0...3].capitalize
|
102
|
+
end
|
103
|
+
opts[:separator] ||= "°"
|
104
|
+
if opts[:latitude] || opts[:longitude]
|
105
|
+
opts[:seconds] ||= false
|
106
|
+
opts[:separator] = neg ? "S" : "N" if opts[:latitude]
|
107
|
+
opts[:separator] = neg ? "E" : "W" if opts[:longitude]
|
108
|
+
opts[:separator] = "º#{opts[:separator]}" if opts[:degree]
|
109
|
+
neg = false
|
110
|
+
d = d.abs
|
111
|
+
output = if opts[:seconds] == false
|
112
|
+
format("%02d%s%02d'", d, opts[:separator], m.round)
|
113
|
+
else
|
114
|
+
format("%02d%s%02d'%02d\"", d, opts[:separator], m.floor, s.round)
|
115
|
+
end
|
116
|
+
elsif opts[:full]
|
117
|
+
output = format("%2d#{opts[:separator]}%2d'%7.4f", d, m.floor, s)
|
118
|
+
elsif opts[:seconds] || (d.zero? && m.floor.zero? && !s.round.zero?)
|
119
|
+
output = format("%02d#{opts[:separator]}%02d'%02d\"", d, m.floor, s.round)
|
120
|
+
else # default to rounded minutes
|
121
|
+
opts[:minutes] ||= "'"
|
122
|
+
output = format("%02d#{opts[:separator]}%02d#{opts[:minutes]}", d, m.round)
|
123
|
+
end
|
124
|
+
if neg
|
125
|
+
output = if output[0] == " "
|
126
|
+
"-#{output[1..]}"
|
127
|
+
else
|
128
|
+
"-#{output}"
|
129
|
+
end
|
130
|
+
elsif opts[:plus]
|
131
|
+
output = if output[0] == " "
|
132
|
+
"+#{output[1..]}"
|
133
|
+
else
|
134
|
+
"+#{output}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
output
|
138
|
+
end
|
139
|
+
|
140
|
+
def sign_to_str(sign, kind = :full)
|
141
|
+
output = sign.capitalize
|
142
|
+
case kind
|
143
|
+
when :glyph
|
144
|
+
SwissEphemeris::ZODIAC_SYM[sign_to_i(sign)]
|
145
|
+
when :short
|
146
|
+
output[0...3]
|
147
|
+
else
|
148
|
+
output
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def deg_to_sign(deg)
|
153
|
+
SwissEphemeris::SIGNS[((deg % 360) / 30).floor]
|
154
|
+
end
|
155
|
+
|
156
|
+
def deg_to_gate(deg)
|
157
|
+
gate, line, * = HumanDesign.deg_to_gate(deg)
|
158
|
+
format("%2d.%1d%1s", gate, line, print_hexagram(gate))
|
159
|
+
end
|
160
|
+
|
161
|
+
def deg_to_s(angle, opts = {})
|
162
|
+
opts = opts.dup
|
163
|
+
opts = { padding: 12, tabs: true, angle: true, glyph: false, prefix: false, seconds: false, gate: false,
|
164
|
+
base: false, house: false, spectrum: false }.merge!(opts)
|
165
|
+
degree, minute, seconds = dms(angle, true)
|
166
|
+
sign = deg_to_sign(angle)
|
167
|
+
output = ""
|
168
|
+
if opts[:angle]
|
169
|
+
if opts[:glyph]
|
170
|
+
# output += sprintf("%s %2dº%2d'", sign_to_str(sign, :glyph), degree, minute.round)
|
171
|
+
output += format("%02d%s%02d", degree, sign_to_str(sign, :glyph), minute.round)
|
172
|
+
else
|
173
|
+
output += format("%2dº %s ", degree, sign_to_str(sign, :short))
|
174
|
+
output += opts[:seconds] ? format("%2d'%2d", minute.floor, seconds.round) : format("%2d'", minute.round)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
output = format("%#{opts[:padding]}s %s", opts[:prefix], output) if opts[:prefix]
|
178
|
+
output += opts[:postfix].to_s if opts[:postfix]
|
179
|
+
|
180
|
+
if (val = opts[:gate] || opts[:spectrum])
|
181
|
+
if val.is_a?(Numeric) # adjust for ayanamsha
|
182
|
+
angle = (angle + val) % 360
|
183
|
+
# $logger.debug("adjusting ayanamsha by #{val}")
|
184
|
+
end
|
185
|
+
gate, line, color, tone, base = HumanDesign.deg_to_gate(angle)
|
186
|
+
if opts[:gate]
|
187
|
+
output += format(" %1s %2d.%1d", print_hexagram(gate), gate, line)
|
188
|
+
output += ".#{color}.#{tone}.#{base}" if opts[:base]
|
189
|
+
end
|
190
|
+
output += " (#{GeneKeys.gate_spectrum(gate)})" if opts[:spectrum]
|
191
|
+
end
|
192
|
+
output = output.gsub("\t", " ") unless opts[:tabs]
|
193
|
+
output.strip if opts[:strip]
|
194
|
+
output
|
195
|
+
end
|
196
|
+
|
197
|
+
def get_timezone_from_latlon(latitude, longitude)
|
198
|
+
response = HTTParty.get("https://api.geoapify.com/v1/geocode/reverse?lat=#{latitude}&lon=#{longitude}&format=json&apiKey=#{API_KEY}")
|
199
|
+
return JSON.parse(response.body)["results"].first["timezone"] if response.success?
|
200
|
+
|
201
|
+
puts "Request failed: #{response.code}"
|
202
|
+
end
|
203
|
+
|
204
|
+
def calculate_aspects(degree1, degree2, orb, max, min_orb = 0)
|
205
|
+
# Create an empty array to store the harmonics
|
206
|
+
harmonics = {}
|
207
|
+
delta = degree1 - degree2
|
208
|
+
# Iterate over the range of harmonics
|
209
|
+
(1..max).each do |harmonic|
|
210
|
+
# Calculate the difference based on the harmonic and default orb value
|
211
|
+
difference = delta * harmonic % 360
|
212
|
+
difference = 360 - difference if difference > 300
|
213
|
+
# Check if the difference is within the orb range
|
214
|
+
next unless difference <= orb && difference > min_orb
|
215
|
+
|
216
|
+
multiple = false
|
217
|
+
if harmonics.any?
|
218
|
+
first = harmonics.first[0]
|
219
|
+
multiple = if first > 1
|
220
|
+
harmonic.gcd(first) > 1
|
221
|
+
else
|
222
|
+
(difference - (harmonics.first[1] * harmonic)) < 0.001
|
223
|
+
end
|
224
|
+
end
|
225
|
+
# If the difference is within the orb range, add the harmonic to the array
|
226
|
+
harmonics[harmonic] = difference unless multiple
|
227
|
+
end
|
228
|
+
harmonics.transform_values{|v| v.round(3) }
|
229
|
+
end
|
230
|
+
|
231
|
+
def calc_all_aspects(planets, opts = {})
|
232
|
+
return if planets.size < 2
|
233
|
+
|
234
|
+
p1 = planets.pop
|
235
|
+
# $logger.debug( "**** Aspects for #{p1.name}" )
|
236
|
+
# $logger.debug( " with #{planets.map(&:abbr)}" )
|
237
|
+
planets.reverse.each do |p2|
|
238
|
+
aspects = p1.%(p2, opts)
|
239
|
+
next unless aspects.any?
|
240
|
+
|
241
|
+
$logger.debug(aspects.map(&:to_s))
|
242
|
+
@aspects += aspects
|
243
|
+
# @aspects << [p1.to_sym, p2.to_sym, aspects.inject({}){|m,e| m.merge(e.to_a.last) } ]
|
244
|
+
end
|
245
|
+
calc_all_aspects(planets, opts)
|
246
|
+
end
|
247
|
+
|
248
|
+
def clear_aspects!
|
249
|
+
@aspects = nil
|
250
|
+
end
|
251
|
+
|
252
|
+
def aspects(planets, _opts = {})
|
253
|
+
opts = { clear_cache: true }.merge!(_opts)
|
254
|
+
if @aspects.nil? || opts[:clear_cache]
|
255
|
+
@aspects = []
|
256
|
+
# $logger.debug "Aspects: opts = #{opts.inspect}"
|
257
|
+
calc_all_aspects(planets.reverse, opts)
|
258
|
+
end
|
259
|
+
[%i[NN SN], %i[MC IC], %i[VX AV], %i[AC DC]].each do |anti|
|
260
|
+
@aspects.reject!{|a| (a.bodies.map(&:abbr) & anti) == anti }
|
261
|
+
end
|
262
|
+
# $logger.debug("DONE! (#{@aspects.count})")
|
263
|
+
@aspects
|
264
|
+
end
|
265
|
+
|
266
|
+
def print_hexagram(idx)
|
267
|
+
(0x4DC0 + idx - 1).to_s(16).to_i(16).chr("UTF-8")
|
268
|
+
end
|
269
|
+
|
270
|
+
def digital_root(num, max = 9)
|
271
|
+
num = num.to_s.chars.map(&:to_i).inject(:+) until num <= max
|
272
|
+
num
|
273
|
+
end
|
274
|
+
|
275
|
+
HOUSE_THEMES = %w[self possessions communication home creativity/pleasure health/service partnerships
|
276
|
+
sex/death/rebirth philosophy/travel career friendships dissolution/karma].unshift(nil)
|
277
|
+
def print_houses(calc, calc_method = nil)
|
278
|
+
output = calc.house_cusps(calc_method).map.with_index do |deg, i|
|
279
|
+
i += 1
|
280
|
+
prefix = format("%-31s", "House #{SwissEphemeris::HOUSES[i]} (#{HOUSE_THEMES[i]}):")
|
281
|
+
ruler = calc.get_body(degree_rulership(deg))
|
282
|
+
tone = ruler.house - i + 1
|
283
|
+
tone += 12 if tone.negative?
|
284
|
+
postfix = " [Lord #{ruler.symbol} in #{format('%3s:%-2s', SwissEphemeris::HOUSES[ruler.house], tone)}] "
|
285
|
+
AstroHelper.deg_to_s(deg, gate: calc.ayanamsha, prefix: prefix, spectrum: true, postfix: postfix)
|
286
|
+
end
|
287
|
+
puts output
|
288
|
+
end
|
289
|
+
|
290
|
+
def name_to_sym(name)
|
291
|
+
name.to_s.delete(" ").sub(".", "_").downcase.to_sym
|
292
|
+
end
|
293
|
+
|
294
|
+
SYMBOLS = { # http://en.wikipedia.org/wiki/Astrological_symbols
|
295
|
+
sun: ["\u2609", "SO"], # "☉"
|
296
|
+
earth: ["\u2295", "EA"], # "⊕"
|
297
|
+
# :earth => ["\u2641", 'EA'], #"♁"
|
298
|
+
moon: ["\u263d", "MO"], # "☾"
|
299
|
+
mercury: ["\u263f", "ME"], # "☿"
|
300
|
+
venus: ["\u2640", "VE"], # "♀"
|
301
|
+
mars: ["\u2642", "MA"], # "♂"
|
302
|
+
jupiter: ["\u2643", "JU"], # "♃"
|
303
|
+
saturn: ["\u2644", "SA"], # "♄"
|
304
|
+
uranus: ["\u2645", "UR"], # "♅"
|
305
|
+
neptune: ["\u2646", "NE"], # "♆"
|
306
|
+
pluto: ["\u2647", "PL"], # "♇"
|
307
|
+
n_node: ["\u260a", "NN"], # "☊"
|
308
|
+
s_node: ["\u260b", "SN"], # "☋"
|
309
|
+
chiron: ["\u26b7", "CH"],
|
310
|
+
ceres: %w[ʡ CE], # "ʡ"
|
311
|
+
pallas: ["\u26b4", "PA"],
|
312
|
+
juno: ["\u26b5", "JN"],
|
313
|
+
vesta: ["\u26b6", "VA"],
|
314
|
+
ascendant: %w[AC AC],
|
315
|
+
descendant: %w[DC DC],
|
316
|
+
midheaven: %w[MC MC],
|
317
|
+
nadir: %w[IC IC],
|
318
|
+
aries: ["♈", "AP"],
|
319
|
+
vertex: ["🜊", "VX"], # TODO: "We surmise that it is possible to introduce another 10 points, complementing the Vertex or Antivertex to a 12-fold division along the prime vertical" # https://web.archive.org/web/20150926042925/http://www.levante.org/svarogich/en/principia_en/part01.html
|
320
|
+
av: ["A🜊", "AV"],
|
321
|
+
retrograde: "\u211e", # "℞",
|
322
|
+
zodiac: ("\u2648".."\u2653").to_a # %w[♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ]
|
323
|
+
}.freeze
|
324
|
+
|
325
|
+
def zodiac_symbol(sign)
|
326
|
+
SYMBOLS[:zodiac][sign_to_i(sign)]
|
327
|
+
end
|
328
|
+
|
329
|
+
def symbol(key)
|
330
|
+
SYMBOLS[key].first
|
331
|
+
end
|
332
|
+
|
333
|
+
def abbr(key)
|
334
|
+
SYMBOLS[key].last.to_sym
|
335
|
+
end
|
336
|
+
|
337
|
+
def abbr_to_key(abbr)
|
338
|
+
SYMBOLS.find{|_k, v| v.last == abbr.to_s }.first
|
339
|
+
end
|
340
|
+
|
341
|
+
# this should work for a calc or a chart
|
342
|
+
def converse(calc, natal_calc)
|
343
|
+
diff = (natal_calc.jd - calc.jd).abs
|
344
|
+
output = calc.dup
|
345
|
+
output.jd = natal_calc.jd - diff
|
346
|
+
output.recalc!
|
347
|
+
output
|
348
|
+
end
|
349
|
+
|
350
|
+
def datetime_to_jd(time = DateTime.now)
|
351
|
+
time = time.utc.to_datetime if time.is_a?(ActiveSupport::TimeWithZone)
|
352
|
+
Swe4r.swe_julday(time.year, time.month, time.day, time.hour + (time.min / 60.0) + (0.1 / 3600.0))
|
353
|
+
end
|
354
|
+
|
355
|
+
def transits(location, t = DateTime.now, **opts)
|
356
|
+
calc = Astroscript::Calculator.new(opts)
|
357
|
+
date = [t.year, t.month, t.day, t.hour, t.min]
|
358
|
+
calc.init(*date, location)
|
359
|
+
end
|
360
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Astroscript
|
4
|
+
class Aspect
|
5
|
+
attr_reader :orb, :aspect, :bodies, :phase
|
6
|
+
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@aspect, :numerator, :denominator
|
9
|
+
def_delegators :denominator, :hph
|
10
|
+
|
11
|
+
FLAVORS = {
|
12
|
+
1r => "conjunct", 1/2r => "opposite", 1/4r => "square",
|
13
|
+
1/8r => "octile", 3/8r => "trioctile",
|
14
|
+
1/3r => "trine", 1/6r => "sextile", # 1/9r => 'novile',
|
15
|
+
1/12r => "semisextile", 5/12r => "quincunx",
|
16
|
+
1/5r => "quintile", 2/5r => "biquintile"
|
17
|
+
# 1/7r => 'septile', 2/7r => 'biseptile', 3/7r => 'triseptile'
|
18
|
+
# 1/10r => 'decile', 1/11r => 'undecile',
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
FLAVORS.each do |r, flavor|
|
22
|
+
define_method "#{flavor}?" do
|
23
|
+
aspect == r
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(b1, b2, opts = {})
|
28
|
+
opts[:max_harmonic] ||= 12
|
29
|
+
opts[:harmonic_orb] ||= 16.0
|
30
|
+
if opts[:ratio] # isolate flavor, e.g. 5/12
|
31
|
+
opts[:harmonic] = opts[:ratio].denominator
|
32
|
+
opts[:numerator] = opts[:ratio].numerator
|
33
|
+
end
|
34
|
+
@bodies = [b1, b2]
|
35
|
+
sort!
|
36
|
+
@phase = @bodies.map(&:lon).diff.first.abs
|
37
|
+
|
38
|
+
ary = if (h = opts[:harmonic]) # isolate single flavor
|
39
|
+
[h]
|
40
|
+
else
|
41
|
+
1..opts[:max_harmonic]
|
42
|
+
end
|
43
|
+
|
44
|
+
ary.each do |harmonic|
|
45
|
+
mod = 360
|
46
|
+
orb = (@phase * harmonic) % mod
|
47
|
+
orb = mod - orb if orb > mod / 2 # this is the "flip" for oppositions
|
48
|
+
|
49
|
+
# check for conjunction in nth harmonic "flavor"
|
50
|
+
next unless orb <= (opts[:orb] || opts[:harmonic_orb])
|
51
|
+
|
52
|
+
if harmonic == 1
|
53
|
+
@aspect = 1r # 1/1 = conjunction
|
54
|
+
else
|
55
|
+
num = (@phase.abs * harmonic / mod).round
|
56
|
+
num = harmonic - num if num > harmonic / 2 # flip e.g. 7/12 to 5/12
|
57
|
+
@aspect = Rational(num, harmonic)
|
58
|
+
end
|
59
|
+
@orb = orb / harmonic
|
60
|
+
isolated_harmonic = opts[:harmonic] && @aspect.denominator != opts[:harmonic]
|
61
|
+
isolated_numerator = opts[:numerator] && @aspect.numerator != opts[:numerator]
|
62
|
+
return self unless isolated_harmonic || isolated_numerator
|
63
|
+
end
|
64
|
+
@aspect = 0r
|
65
|
+
@orb = nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def ==(other)
|
69
|
+
bodies.map(&:abbr).sort == other.bodies.map(&:abbr).sort && phase == other.phase
|
70
|
+
end
|
71
|
+
alias eql? ==
|
72
|
+
|
73
|
+
def overlap?
|
74
|
+
abbrs = @bodies.map{|b| b.abbr.to_s.split("/").flatten }
|
75
|
+
(abbrs.first & abbrs.last).any?
|
76
|
+
end
|
77
|
+
|
78
|
+
def sort!
|
79
|
+
@bodies.sort_by!{|x| x.is_a?(Midpoint) ? x.bodies.first.speed_order.to_i : x.speed_order.to_i }
|
80
|
+
b1 = @bodies.first
|
81
|
+
b2 = @bodies.last
|
82
|
+
if b1.is_a?(Midpoint)
|
83
|
+
if b2.is_a?(Midpoint)
|
84
|
+
if b1.bodies.first == b2.bodies.first && b1.bodies.last.speed_order.to_i < (b2.bodies.last.speed_order.to_i)
|
85
|
+
@bodies.reverse!
|
86
|
+
end
|
87
|
+
else
|
88
|
+
@bodies.reverse!
|
89
|
+
end
|
90
|
+
end
|
91
|
+
@bodies.reverse! if @bodies.last.prefix
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
def applying?
|
96
|
+
return @applying if @applying
|
97
|
+
|
98
|
+
@orb1 = @bodies.map(&:degree).inject(:-).abs
|
99
|
+
body = @bodies.first
|
100
|
+
if body.is_a?(Midpoint)
|
101
|
+
body = @bodies.last
|
102
|
+
return nil if body.is_a?(Midpoint)
|
103
|
+
end
|
104
|
+
body.calc.jd += 1 # move forward
|
105
|
+
@bodies.each(&:calculate!)
|
106
|
+
@orb2 = @bodies.map(&:degree).inject(:-).abs
|
107
|
+
body.calc.jd -= 1 # put back
|
108
|
+
@bodies.each(&:calculate!)
|
109
|
+
return nil if @orb1 == @orb2
|
110
|
+
|
111
|
+
@applying = @orb1 > @orb2
|
112
|
+
end
|
113
|
+
|
114
|
+
def separating?
|
115
|
+
applying? # calculate stuff
|
116
|
+
return nil if @orb1 == @orb2
|
117
|
+
|
118
|
+
@orb1 < @orb2
|
119
|
+
end
|
120
|
+
|
121
|
+
def ratio
|
122
|
+
"#{numerator}/#{denominator}"
|
123
|
+
end
|
124
|
+
|
125
|
+
def valid?
|
126
|
+
!invalid?
|
127
|
+
end
|
128
|
+
|
129
|
+
def invalid?
|
130
|
+
numerator.zero? || !FLAVORS.keys.include?(aspect)
|
131
|
+
end
|
132
|
+
|
133
|
+
def to_a
|
134
|
+
@bodies.map(&:to_sym) << { harmonic => orb.round(3) }
|
135
|
+
end
|
136
|
+
|
137
|
+
def to_s(opts = {})
|
138
|
+
opts[:symbol] = true unless opts.key?(:symbol)
|
139
|
+
opts[:orb] = true unless opts.key?(:orb)
|
140
|
+
return "invalid: #{inspect}" unless valid?
|
141
|
+
|
142
|
+
flavor = FLAVORS[aspect]
|
143
|
+
flavor = SwissEphemeris::SYMBOLS[flavor.to_sym] if opts[:symbol]
|
144
|
+
output = "#{@bodies.first.handle} #{flavor} #{@bodies.last.handle}"
|
145
|
+
output += " [#{@orb.to_dms(separator: applying? ? 'a' : 's')}]" if opts[:orb]
|
146
|
+
output
|
147
|
+
end
|
148
|
+
|
149
|
+
def antiphase
|
150
|
+
-@phase % 360
|
151
|
+
end
|
152
|
+
|
153
|
+
def sum
|
154
|
+
[b1, b2].map{|b| b.send(@method) }.sum % 360
|
155
|
+
end
|
156
|
+
|
157
|
+
def direct_midpoint?
|
158
|
+
return unless midpoint?
|
159
|
+
|
160
|
+
@bodies.map(&:lon).diff.first.abs < 90
|
161
|
+
end
|
162
|
+
|
163
|
+
def midpoint?
|
164
|
+
@bodies.any?{|b| b.is_a?(Midpoint) }
|
165
|
+
end
|
166
|
+
|
167
|
+
def isotrap?
|
168
|
+
@bodies.all?{|b| b.is_a?(Midpoint) }
|
169
|
+
end
|
170
|
+
|
171
|
+
def opposite_midpoint?
|
172
|
+
!direct?
|
173
|
+
end
|
174
|
+
|
175
|
+
def midpoint_aspect
|
176
|
+
return unless midpoint?
|
177
|
+
|
178
|
+
direct_midpoint? ? SwissEphemeris::SYMBOLS[:conjunct] : SwissEphemeris::SYMBOLS[:opposite]
|
179
|
+
end
|
180
|
+
|
181
|
+
def print_midpoint
|
182
|
+
return unless midpoint?
|
183
|
+
|
184
|
+
movement = applying? ? "a" : "s"
|
185
|
+
"#{@bodies.map(&:symbol).join(" #{midpoint_aspect} ")}\t[#{AstroHelper.print_dms(orb)}#{movement}]"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Astroscript
|
4
|
+
class ConstBody < Body
|
5
|
+
attr_accessor :symbol, :name, :lon
|
6
|
+
alias degree lon
|
7
|
+
def initialize(deg, name, symbol = nil, abbr = nil)
|
8
|
+
@lon = deg
|
9
|
+
@name = name
|
10
|
+
@harmonic = 1
|
11
|
+
@abbr = abbr || name[0..1].upcase.to_sym
|
12
|
+
@symbol = symbol || @abbr
|
13
|
+
@calc = NullCalc.new(@lon)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_sym
|
17
|
+
@abbr
|
18
|
+
end
|
19
|
+
|
20
|
+
def calculate!
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
class NullCalc # Silently ignores any method call
|
25
|
+
def initialize(lon)
|
26
|
+
@lon = lon
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_body **_args
|
30
|
+
@lon
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(method_name, *_args)
|
34
|
+
case method_name
|
35
|
+
when %i[to_f]
|
36
|
+
0.0
|
37
|
+
when %i[to_i + - / * %]
|
38
|
+
0
|
39
|
+
when :prefix
|
40
|
+
""
|
41
|
+
else
|
42
|
+
self
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def coerce(_other)
|
51
|
+
[0, 0] # or [other, 0] depending on your needs
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|