astrolith 0.1.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.
@@ -0,0 +1,64 @@
1
+ require_relative 'base_chart'
2
+
3
+ module RubyChartEngine
4
+ module Charts
5
+ class Transit < BaseChart
6
+ attr_reader :natal_chart, :transit_aspects
7
+
8
+ # Transit chart - current planetary positions compared to natal chart
9
+ def initialize(natal_chart_params:, transit_datetime:, house_system: :placidus)
10
+ # Create natal chart if not provided
11
+ @natal_chart = if natal_chart_params.is_a?(Natal)
12
+ natal_chart_params
13
+ else
14
+ Natal.new(**natal_chart_params)
15
+ end
16
+
17
+ # Use natal chart location for transit chart
18
+ # (transits are typically calculated for the natal location)
19
+ super(
20
+ datetime: transit_datetime,
21
+ latitude: @natal_chart.coordinates.latitude,
22
+ longitude: @natal_chart.coordinates.longitude,
23
+ timezone: natal_chart_params[:timezone] || 'UTC',
24
+ house_system: house_system
25
+ )
26
+
27
+ # Calculate aspects between transit planets and natal planets
28
+ calculate_transit_aspects!
29
+ end
30
+
31
+ def to_hash
32
+ super.merge(
33
+ chart_type: 'transit',
34
+ natal_chart_metadata: @natal_chart.to_hash[:metadata],
35
+ transit_aspects: @transit_aspects
36
+ )
37
+ end
38
+
39
+ private
40
+
41
+ def calculate_transit_aspects!
42
+ # Calculate aspects from transit planets to natal planets
43
+ @transit_aspects = Calculations::Aspects.calculate(
44
+ @planets,
45
+ @natal_chart.planets
46
+ )
47
+
48
+ # Also calculate aspects from natal to transit (for completeness)
49
+ reverse_aspects = Calculations::Aspects.calculate(
50
+ @natal_chart.planets,
51
+ @planets
52
+ )
53
+
54
+ # Combine and deduplicate
55
+ @transit_aspects = (@transit_aspects + reverse_aspects).uniq do |aspect|
56
+ [
57
+ [aspect[:planet1], aspect[:planet2]].sort,
58
+ aspect[:type]
59
+ ]
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,49 @@
1
+ module RubyChartEngine
2
+ module Input
3
+ class Coordinates
4
+ attr_reader :latitude, :longitude
5
+
6
+ # Parse coordinates from various formats:
7
+ # - Decimal degrees: { lat: 32.7157, lon: -117.1611 }
8
+ # - Standard text: { lat: '32n43', lon: '117w10' }
9
+ # - Mixed: { lat: 32.7157, lon: '117w10' }
10
+ def initialize(lat:, lon:)
11
+ @latitude = parse_coordinate(lat, :lat)
12
+ @longitude = parse_coordinate(lon, :lon)
13
+ end
14
+
15
+ private
16
+
17
+ def parse_coordinate(value, type)
18
+ return value.to_f if value.is_a?(Numeric)
19
+ return value.to_f if value.is_a?(String) && value.match?(/^-?\d+\.?\d*$/)
20
+
21
+ # Parse standard text format like '32n43' or '117w10'
22
+ parse_standard_text(value, type)
23
+ end
24
+
25
+ def parse_standard_text(value, type)
26
+ # Match patterns like: 32n43, 32N43, 32n43'30", etc.
27
+ match = value.match(/(\d+)([nsewNSEW])(\d+)?(?:'(\d+)")?/)
28
+
29
+ raise Error, "Invalid coordinate format: #{value}" unless match
30
+
31
+ degrees = match[1].to_i
32
+ direction = match[2].downcase
33
+ minutes = match[3]&.to_i || 0
34
+ seconds = match[4]&.to_i || 0
35
+
36
+ decimal = degrees + (minutes / 60.0) + (seconds / 3600.0)
37
+
38
+ # Apply sign based on direction
39
+ if type == :lat
40
+ decimal = -decimal if direction == 's'
41
+ else
42
+ decimal = -decimal if direction == 'w'
43
+ end
44
+
45
+ decimal
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,68 @@
1
+ require 'time'
2
+
3
+ module RubyChartEngine
4
+ module Input
5
+ class DateTime
6
+ attr_reader :year, :month, :day, :hour, :minute, :second, :datetime
7
+
8
+ # Parse datetime from various formats:
9
+ # - Ruby DateTime/Time object
10
+ # - ISO 8601 string: "2024-03-15T14:30:00"
11
+ # - Hash: { year: 2024, month: 3, day: 15, hour: 14, minute: 30 }
12
+ def initialize(datetime)
13
+ @datetime = parse_datetime(datetime)
14
+ @year = @datetime.year
15
+ @month = @datetime.month
16
+ @day = @datetime.day
17
+ @hour = @datetime.hour
18
+ @minute = @datetime.min
19
+ @second = @datetime.sec
20
+ end
21
+
22
+ # Convert to Julian Day Number for Swiss Ephemeris
23
+ def to_julian_day(timezone_offset = 0)
24
+ # Adjust for timezone offset (in hours)
25
+ adjusted_time = @datetime - (timezone_offset * 3600)
26
+
27
+ # Use Swiss Ephemeris to calculate Julian Day
28
+ # swe_julday takes 4 arguments: year, month, day, hour (as decimal)
29
+ Swe4r.swe_julday(
30
+ adjusted_time.year,
31
+ adjusted_time.month,
32
+ adjusted_time.day,
33
+ adjusted_time.hour + (adjusted_time.min / 60.0) + (adjusted_time.sec / 3600.0)
34
+ )
35
+ end
36
+
37
+ private
38
+
39
+ def parse_datetime(value)
40
+ case value
41
+ when ::DateTime, ::Time
42
+ value.to_datetime
43
+ when String
44
+ ::DateTime.parse(value)
45
+ when Hash
46
+ validate_datetime_hash!(value)
47
+ ::DateTime.new(
48
+ value[:year],
49
+ value[:month],
50
+ value[:day],
51
+ value[:hour] || 0,
52
+ value[:minute] || 0,
53
+ value[:second] || 0
54
+ )
55
+ else
56
+ raise Error, "Invalid datetime format: #{value.class}"
57
+ end
58
+ end
59
+
60
+ def validate_datetime_hash!(hash)
61
+ required = [:year, :month, :day]
62
+ missing = required - hash.keys
63
+
64
+ raise Error, "Missing required datetime fields: #{missing.join(', ')}" if missing.any?
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,76 @@
1
+ require 'tzinfo'
2
+
3
+ module RubyChartEngine
4
+ module Input
5
+ class Timezone
6
+ attr_reader :timezone, :offset
7
+
8
+ # Parse timezone from various formats:
9
+ # - TZInfo timezone identifier: "America/Los_Angeles"
10
+ # - UTC offset: "+05:30" or -8
11
+ # - TZInfo::Timezone object
12
+ def initialize(timezone, datetime = nil)
13
+ @timezone = parse_timezone(timezone)
14
+ @datetime = datetime
15
+ @offset = calculate_offset
16
+ end
17
+
18
+ # Get offset in hours
19
+ def offset_hours
20
+ @offset / 3600.0
21
+ end
22
+
23
+ private
24
+
25
+ def parse_timezone(value)
26
+ case value
27
+ when TZInfo::Timezone
28
+ value
29
+ when String
30
+ if value.match?(/^[+-]?\d{1,2}:?\d{0,2}$/)
31
+ # UTC offset format
32
+ parse_utc_offset(value)
33
+ else
34
+ # TZInfo identifier
35
+ TZInfo::Timezone.get(value)
36
+ end
37
+ when Numeric
38
+ # Hours offset
39
+ parse_utc_offset(value)
40
+ else
41
+ raise Error, "Invalid timezone format: #{value.class}"
42
+ end
43
+ end
44
+
45
+ def parse_utc_offset(value)
46
+ # Convert to seconds offset
47
+ case value
48
+ when String
49
+ sign = value.start_with?('-') ? -1 : 1
50
+ parts = value.gsub(/^[+-]/, '').split(':')
51
+ hours = parts[0].to_i
52
+ minutes = parts[1]&.to_i || 0
53
+ offset_seconds = sign * (hours * 3600 + minutes * 60)
54
+ when Numeric
55
+ offset_seconds = value * 3600
56
+ end
57
+
58
+ # Create a timezone with fixed offset
59
+ TZInfo::Timezone.get('UTC').tap do |tz|
60
+ @offset = offset_seconds
61
+ end
62
+ end
63
+
64
+ def calculate_offset
65
+ return @offset if @offset
66
+
67
+ if @timezone.is_a?(TZInfo::Timezone) && @datetime
68
+ period = @timezone.period_for_local(@datetime)
69
+ period.utc_total_offset
70
+ else
71
+ 0
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,44 @@
1
+ module RubyChartEngine
2
+ module Serializers
3
+ class JsonSerializer
4
+ def self.serialize(chart)
5
+ chart.to_hash.to_json
6
+ end
7
+
8
+ def self.serialize_pretty(chart)
9
+ JSON.pretty_generate(chart.to_hash)
10
+ end
11
+
12
+ # Serialize multiple charts
13
+ def self.serialize_collection(charts)
14
+ charts.map(&:to_hash).to_json
15
+ end
16
+
17
+ # Serialize with custom options
18
+ def self.serialize_with_options(chart, options = {})
19
+ data = chart.to_hash
20
+
21
+ # Apply filters if specified
22
+ if options[:include]
23
+ data = data.select { |k, _| options[:include].include?(k) }
24
+ end
25
+
26
+ if options[:exclude]
27
+ data = data.reject { |k, _| options[:exclude].include?(k) }
28
+ end
29
+
30
+ # Format output
31
+ if options[:pretty]
32
+ JSON.pretty_generate(data)
33
+ else
34
+ data.to_json
35
+ end
36
+ end
37
+
38
+ # Parse JSON back to hash
39
+ def self.deserialize(json_string)
40
+ JSON.parse(json_string, symbolize_names: true)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module RubyChartEngine
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,82 @@
1
+ require 'swe4r'
2
+ require 'ephemeris'
3
+ require 'daru'
4
+ require 'json'
5
+
6
+ require_relative 'ruby_chart_engine/version'
7
+ require_relative 'ruby_chart_engine/input/coordinates'
8
+ require_relative 'ruby_chart_engine/input/datetime'
9
+ require_relative 'ruby_chart_engine/input/timezone'
10
+
11
+ require_relative 'ruby_chart_engine/calculations/positions'
12
+ require_relative 'ruby_chart_engine/calculations/houses'
13
+ require_relative 'ruby_chart_engine/calculations/aspects'
14
+ require_relative 'ruby_chart_engine/calculations/dignities'
15
+
16
+ require_relative 'ruby_chart_engine/serializers/json_serializer'
17
+
18
+ require_relative 'ruby_chart_engine/charts/base_chart'
19
+ require_relative 'ruby_chart_engine/charts/natal'
20
+ require_relative 'ruby_chart_engine/charts/solar_return'
21
+ require_relative 'ruby_chart_engine/charts/progressed'
22
+ require_relative 'ruby_chart_engine/charts/composite'
23
+ require_relative 'ruby_chart_engine/charts/transit'
24
+
25
+ module RubyChartEngine
26
+ class Error < StandardError; end
27
+
28
+ # Swiss Ephemeris object ID constants
29
+ # These are standard SE values used across all implementations
30
+ PLANETS = {
31
+ sun: 0, # SE_SUN
32
+ moon: 1, # SE_MOON
33
+ mercury: 2, # SE_MERCURY
34
+ venus: 3, # SE_VENUS
35
+ mars: 4, # SE_MARS
36
+ jupiter: 5, # SE_JUPITER
37
+ saturn: 6, # SE_SATURN
38
+ uranus: 7, # SE_URANUS
39
+ neptune: 8, # SE_NEPTUNE
40
+ pluto: 9 # SE_PLUTO
41
+ }.freeze
42
+
43
+ POINTS = {
44
+ north_node: 10, # SE_TRUE_NODE (or use 11 for mean node)
45
+ south_node: 10, # Will be calculated as opposite of north node
46
+ chiron: 15 # SE_CHIRON
47
+ }.freeze
48
+
49
+ ANGLES = {
50
+ ascendant: :asc,
51
+ midheaven: :mc,
52
+ descendant: :dsc,
53
+ imum_coeli: :ic
54
+ }.freeze
55
+
56
+ # Zodiac signs
57
+ SIGNS = [
58
+ { name: 'Aries', element: 'Fire', modality: 'Cardinal' },
59
+ { name: 'Taurus', element: 'Earth', modality: 'Fixed' },
60
+ { name: 'Gemini', element: 'Air', modality: 'Mutable' },
61
+ { name: 'Cancer', element: 'Water', modality: 'Cardinal' },
62
+ { name: 'Leo', element: 'Fire', modality: 'Fixed' },
63
+ { name: 'Virgo', element: 'Earth', modality: 'Mutable' },
64
+ { name: 'Libra', element: 'Air', modality: 'Cardinal' },
65
+ { name: 'Scorpio', element: 'Water', modality: 'Fixed' },
66
+ { name: 'Sagittarius', element: 'Fire', modality: 'Mutable' },
67
+ { name: 'Capricorn', element: 'Earth', modality: 'Cardinal' },
68
+ { name: 'Aquarius', element: 'Air', modality: 'Fixed' },
69
+ { name: 'Pisces', element: 'Water', modality: 'Mutable' }
70
+ ].freeze
71
+
72
+ # House systems
73
+ HOUSE_SYSTEMS = {
74
+ placidus: 'P',
75
+ koch: 'K',
76
+ porphyrius: 'O',
77
+ regiomontanus: 'R',
78
+ campanus: 'C',
79
+ equal: 'E',
80
+ whole_sign: 'W'
81
+ }.freeze
82
+ end
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: astrolith
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Go-pye
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-12-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: swe4r
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: ephemeris
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: daru
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: tzinfo
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: apexcharts
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: prawn
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.4'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.12'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.12'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.14'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.14'
125
+ description: Astrolith is a decoupled astrological data calculation engine that generates
126
+ structured JSON output. It supports multiple chart types including Natal, Solar
127
+ Return, Progressed, Composite, and Transit charts.
128
+ email:
129
+ - Go-pye@users.noreply.github.com
130
+ executables: []
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - LICENSE
135
+ - README.md
136
+ - astrolith.gemspec
137
+ - examples/example_usage.rb
138
+ - lib/ruby_chart_engine.rb
139
+ - lib/ruby_chart_engine/calculations/aspects.rb
140
+ - lib/ruby_chart_engine/calculations/dignities.rb
141
+ - lib/ruby_chart_engine/calculations/houses.rb
142
+ - lib/ruby_chart_engine/calculations/positions.rb
143
+ - lib/ruby_chart_engine/charts/base_chart.rb
144
+ - lib/ruby_chart_engine/charts/composite.rb
145
+ - lib/ruby_chart_engine/charts/natal.rb
146
+ - lib/ruby_chart_engine/charts/progressed.rb
147
+ - lib/ruby_chart_engine/charts/solar_return.rb
148
+ - lib/ruby_chart_engine/charts/transit.rb
149
+ - lib/ruby_chart_engine/input/coordinates.rb
150
+ - lib/ruby_chart_engine/input/datetime.rb
151
+ - lib/ruby_chart_engine/input/timezone.rb
152
+ - lib/ruby_chart_engine/serializers/json_serializer.rb
153
+ - lib/ruby_chart_engine/version.rb
154
+ homepage: https://github.com/Go-pye/astrolith
155
+ licenses:
156
+ - MIT
157
+ metadata: {}
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: 2.7.0
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ requirements: []
173
+ rubygems_version: 3.3.7
174
+ signing_key:
175
+ specification_version: 4
176
+ summary: A Ruby port of immanuel-python astrology library
177
+ test_files: []