astroscript 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|