astro_chart 0.1.0 → 0.2.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/LICENSE +17 -16
  4. data/astro_chart.gemspec +7 -6
  5. data/lib/astro_chart/ephemeris.rb +76 -18
  6. data/lib/astro_chart/pure/core.rb +209 -0
  7. data/lib/astro_chart/pure/houses.rb +122 -0
  8. data/lib/astro_chart/pure/moon.rb +341 -0
  9. data/lib/astro_chart/pure/moon_elp.rb +931 -0
  10. data/lib/astro_chart/pure/pluto.rb +220 -0
  11. data/lib/astro_chart/pure/vsop87.rb +152 -0
  12. data/lib/astro_chart/pure/vsop87_data.rb +210 -0
  13. data/lib/astro_chart/pure.rb +63 -0
  14. data/lib/astro_chart/synastry.rb +118 -0
  15. data/lib/astro_chart/version.rb +1 -1
  16. data/lib/astro_chart.rb +6 -1
  17. metadata +19 -45
  18. data/ext/astro_chart/astro_chart_ext.c +0 -99
  19. data/ext/astro_chart/extconf.rb +0 -8
  20. data/ext/astro_chart/swecl.c +0 -6428
  21. data/ext/astro_chart/swedate.c +0 -588
  22. data/ext/astro_chart/swedate.h +0 -81
  23. data/ext/astro_chart/swedll.h +0 -403
  24. data/ext/astro_chart/sweephe4.c +0 -702
  25. data/ext/astro_chart/sweephe4.h +0 -239
  26. data/ext/astro_chart/swehel.c +0 -3511
  27. data/ext/astro_chart/swehouse.c +0 -3143
  28. data/ext/astro_chart/swehouse.h +0 -98
  29. data/ext/astro_chart/swejpl.c +0 -958
  30. data/ext/astro_chart/swejpl.h +0 -103
  31. data/ext/astro_chart/swemmoon.c +0 -1930
  32. data/ext/astro_chart/swemplan.c +0 -967
  33. data/ext/astro_chart/swemptab.h +0 -10640
  34. data/ext/astro_chart/swenut2000a.h +0 -2819
  35. data/ext/astro_chart/sweodef.h +0 -326
  36. data/ext/astro_chart/sweph.c +0 -8614
  37. data/ext/astro_chart/sweph.h +0 -849
  38. data/ext/astro_chart/swephexp.h +0 -1020
  39. data/ext/astro_chart/swephlib.c +0 -4634
  40. data/ext/astro_chart/swephlib.h +0 -189
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pure/core"
4
+ require_relative "pure/houses"
5
+ require_relative "pure/vsop87"
6
+ require_relative "pure/moon"
7
+ require_relative "pure/pluto"
8
+
9
+ module AstroChart
10
+ # Pure-Ruby ephemeris backend (MIT-safe, zero external dependencies).
11
+ #
12
+ # Front-facing API mirrors the Swiss Ephemeris C extension (AstroChart::Ext):
13
+ # Pure.julday(year, month, day, hour) -> JD(UT) Float
14
+ # Pure.calc_ut(jd_ut, planet_id) -> apparent ecliptic longitude (deg, 0-360)
15
+ # Pure.houses(jd_ut, lat, lon, hsys = "P") -> { "cusps" => [12], "ascendant" => f, "mc" => f }
16
+ #
17
+ # planet_id follows the SE convention:
18
+ # SUN=0 MOON=1 MERCURY=2 VENUS=3 MARS=4 JUPITER=5 SATURN=6
19
+ # URANUS=7 NEPTUNE=8 PLUTO=9 TRUE_NODE=11
20
+ module Pure
21
+ # planet_id (SE convention) -> handled by which pure module
22
+ VSOP87_PLANET_IDS = [0, 2, 3, 4, 5, 6, 7, 8].freeze
23
+ MOON_ID = 1
24
+ PLUTO_ID = 9
25
+ TRUE_NODE_ID = 11
26
+
27
+ module_function
28
+
29
+ # Convert calendar date/time (UT) to Julian Day number.
30
+ def julday(year, month, day, hour)
31
+ Core.julday(year, month, day, hour)
32
+ end
33
+
34
+ # Apparent ecliptic longitude (degrees, [0, 360)) for the given body.
35
+ def calc_ut(jd_ut, planet_id)
36
+ case planet_id
37
+ when *VSOP87_PLANET_IDS
38
+ Vsop87.apparent_longitude(planet_id, jd_ut)
39
+ when MOON_ID
40
+ Moon.apparent_longitude(jd_ut)
41
+ when PLUTO_ID
42
+ Pluto.apparent_longitude(jd_ut)
43
+ when TRUE_NODE_ID
44
+ Moon.true_node(jd_ut)
45
+ else
46
+ raise ArgumentError,
47
+ "unsupported planet_id #{planet_id.inspect} " \
48
+ "(supported: 0-9 and 11/TRUE_NODE, SE convention)"
49
+ end
50
+ end
51
+
52
+ # House cusps + ascendant + MC. Only Placidus is implemented.
53
+ # hsys accepts "P" or 80 ("P".ord) to mirror the C extension's int argument.
54
+ def houses(jd_ut, latitude, longitude, hsys = "P")
55
+ unless hsys == "P" || hsys == 80
56
+ raise ArgumentError,
57
+ "unsupported house system #{hsys.inspect} (pure backend only supports Placidus: \"P\" / 80)"
58
+ end
59
+
60
+ Houses.calc(jd_ut, latitude, longitude)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,118 @@
1
+ module AstroChart
2
+ # Synastry (合盤): cross-chart comparison between two natal charts.
3
+ #
4
+ # Pure composition of existing modules — Aspects.calculate works on any two
5
+ # ecliptic longitudes regardless of which chart they come from, and
6
+ # Houses.find_house places any longitude into any set of cusps.
7
+ # Backend-independent: no ephemeris call happens here.
8
+ #
9
+ # Typical usage with two Chart#generate results:
10
+ #
11
+ # result = Synastry.between(chart_a, chart_b)
12
+ # result["aspects"] # A 的行星 × B 的行星 的跨盤相位
13
+ # result["a_planets_in_b_houses"] # A 的行星落在 B 的哪一宮(疊盤)
14
+ # result["b_planets_in_a_houses"] # B 的行星落在 A 的哪一宮
15
+ module Synastry
16
+ # Bodies used for cross-chart comparison.
17
+ # 南交點 is excluded by default: it is always exactly opposite 北交點,
18
+ # so including it would mirror every node aspect and double the noise.
19
+ BODIES = Ephemeris::PLANETS.keys.freeze
20
+
21
+ # Cross aspects between two position sets.
22
+ #
23
+ # positions_a / positions_b: { "太陽" => 123.45, ... } (ecliptic longitudes)
24
+ # orb_limit: optional Float — keep only aspects with orb <= limit.
25
+ # (Aspects uses natal orbs: conjunction 15°, others 6-10°. Synastry
26
+ # practice often uses tighter orbs; pass e.g. orb_limit: 6.0 to tighten.)
27
+ #
28
+ # Returns Array of:
29
+ # { "a_planet" => "太陽", "b_planet" => "月亮",
30
+ # "aspect_type" => "三分相", "orb" => 1.23 }
31
+ #
32
+ # Note: (A太陽, B月亮) and (A月亮, B太陽) are different pairs —
33
+ # they compare different positions, both are kept.
34
+ def self.cross_aspects(positions_a, positions_b, orb_limit: nil)
35
+ results = []
36
+ positions_a.each do |name_a, pos_a|
37
+ positions_b.each do |name_b, pos_b|
38
+ aspect_type, orb = Aspects.calculate(pos_a, pos_b)
39
+ next if aspect_type.nil?
40
+ next if orb_limit && orb > orb_limit
41
+
42
+ results << {
43
+ "a_planet" => name_a,
44
+ "b_planet" => name_b,
45
+ "aspect_type" => aspect_type,
46
+ "orb" => orb,
47
+ }
48
+ end
49
+ end
50
+ results.sort_by { |r| r["orb"] }
51
+ end
52
+
53
+ # House overlay (疊盤): place one person's planets into the other
54
+ # person's houses.
55
+ #
56
+ # positions: { "太陽" => 123.45, ... }
57
+ # cusps: 12 house cusp degrees (cusps[0] = 1st house)
58
+ #
59
+ # Returns { "太陽" => 7, ... } (house number 1-12)
60
+ def self.house_overlay(positions, cusps)
61
+ positions.each_with_object({}) do |(name, pos), out|
62
+ house = Houses.find_house(pos, cusps)
63
+ out[name] = house if house
64
+ end
65
+ end
66
+
67
+ # Full synastry between two Chart#generate result hashes.
68
+ def self.between(chart_a, chart_b, orb_limit: nil)
69
+ pos_a = positions_from_chart(chart_a)
70
+ pos_b = positions_from_chart(chart_b)
71
+ cusps_a = cusps_from_chart(chart_a)
72
+ cusps_b = cusps_from_chart(chart_b)
73
+
74
+ {
75
+ "aspects" => cross_aspects(pos_a, pos_b, orb_limit: orb_limit),
76
+ "a_planets_in_b_houses" => house_overlay(pos_a, cusps_b),
77
+ "b_planets_in_a_houses" => house_overlay(pos_b, cusps_a),
78
+ }
79
+ end
80
+
81
+ # Extract { name => total_degree } for BODIES from a Chart#generate hash.
82
+ # Ruler points appended by key_points_data are ignored (not in BODIES).
83
+ def self.positions_from_chart(chart)
84
+ planets = chart&.dig("chart", "planets")
85
+ raise ArgumentError, "chart has no planets data" if planets.nil? || planets.empty?
86
+
87
+ planets.each_with_object({}) do |p, out|
88
+ name = p["planet"]
89
+ next unless BODIES.include?(name)
90
+
91
+ degree = p["total_degree"] || total_degree_from(p)
92
+ out[name] = degree if degree
93
+ end
94
+ end
95
+
96
+ # Extract the 12 cusp degrees (index 0 = 1st house) from a Chart#generate hash.
97
+ def self.cusps_from_chart(chart)
98
+ houses = chart&.dig("chart", "houses")
99
+ raise ArgumentError, "chart has no houses data" if houses.nil? || houses.length != 12
100
+
101
+ houses.sort_by { |h| h["house_number"] }.map { |h| h["degree"] }
102
+ end
103
+
104
+ # Fallback for older snapshots that only stored zodiac + in-sign degree.
105
+ def self.total_degree_from(planet)
106
+ zodiac = planet["zodiac"]
107
+ degree = planet["degree"]
108
+ return nil if zodiac.nil? || degree.nil?
109
+
110
+ sign_index = Zodiac::SIGNS.index(zodiac)
111
+ return nil if sign_index.nil?
112
+
113
+ sign_index * 30.0 + degree
114
+ end
115
+
116
+ private_class_method :total_degree_from
117
+ end
118
+ end
@@ -1,3 +1,3 @@
1
1
  module AstroChart
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/astro_chart.rb CHANGED
@@ -1,11 +1,16 @@
1
1
  require_relative "astro_chart/version"
2
- require_relative "astro_chart/astro_chart_ext"
2
+ # NOTE: the Swiss Ephemeris C extension (astro_chart_ext) is NOT loaded here.
3
+ # The default backend is pure Ruby; the extension is required on demand by
4
+ # `AstroChart.backend = :swiss` so environments without a compiled extension
5
+ # still work out of the box.
6
+ require_relative "astro_chart/pure"
3
7
  require_relative "astro_chart/ephemeris"
4
8
  require_relative "astro_chart/zodiac"
5
9
  require_relative "astro_chart/aspects"
6
10
  require_relative "astro_chart/houses"
7
11
  require_relative "astro_chart/time_conversion"
8
12
  require_relative "astro_chart/planets"
13
+ require_relative "astro_chart/synastry"
9
14
  require_relative "astro_chart/chart"
10
15
 
11
16
  module AstroChart
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: astro_chart
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Huang Yudi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-01 00:00:00.000000000 Z
11
+ date: 2026-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tzinfo
@@ -52,65 +52,39 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '13.0'
55
- - !ruby/object:Gem::Dependency
56
- name: rake-compiler
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '1.2'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '1.2'
69
- description: A Ruby gem for natal astrology chart calculation, powered by Swiss Ephemeris
70
- C library with Moshier ephemeris. No external data files needed.
55
+ description: 'Natal astrology chart calculation in pure Ruby: apparent planetary longitudes
56
+ (VSOP87D, ELP-2000/82B moon, Meeus Pluto), Placidus houses, aspects and synastry.
57
+ No C extension, no external data files. Implemented from public formulas (Meeus,
58
+ Astronomical Algorithms 2nd ed.).'
71
59
  email:
72
60
  executables: []
73
- extensions:
74
- - ext/astro_chart/extconf.rb
61
+ extensions: []
75
62
  extra_rdoc_files: []
76
63
  files:
64
+ - CHANGELOG.md
77
65
  - LICENSE
78
66
  - astro_chart.gemspec
79
- - ext/astro_chart/astro_chart_ext.c
80
- - ext/astro_chart/extconf.rb
81
- - ext/astro_chart/swecl.c
82
- - ext/astro_chart/swedate.c
83
- - ext/astro_chart/swedate.h
84
- - ext/astro_chart/swedll.h
85
- - ext/astro_chart/sweephe4.c
86
- - ext/astro_chart/sweephe4.h
87
- - ext/astro_chart/swehel.c
88
- - ext/astro_chart/swehouse.c
89
- - ext/astro_chart/swehouse.h
90
- - ext/astro_chart/swejpl.c
91
- - ext/astro_chart/swejpl.h
92
- - ext/astro_chart/swemmoon.c
93
- - ext/astro_chart/swemplan.c
94
- - ext/astro_chart/swemptab.h
95
- - ext/astro_chart/swenut2000a.h
96
- - ext/astro_chart/sweodef.h
97
- - ext/astro_chart/sweph.c
98
- - ext/astro_chart/sweph.h
99
- - ext/astro_chart/swephexp.h
100
- - ext/astro_chart/swephlib.c
101
- - ext/astro_chart/swephlib.h
102
67
  - lib/astro_chart.rb
103
68
  - lib/astro_chart/aspects.rb
104
69
  - lib/astro_chart/chart.rb
105
70
  - lib/astro_chart/ephemeris.rb
106
71
  - lib/astro_chart/houses.rb
107
72
  - lib/astro_chart/planets.rb
73
+ - lib/astro_chart/pure.rb
74
+ - lib/astro_chart/pure/core.rb
75
+ - lib/astro_chart/pure/houses.rb
76
+ - lib/astro_chart/pure/moon.rb
77
+ - lib/astro_chart/pure/moon_elp.rb
78
+ - lib/astro_chart/pure/pluto.rb
79
+ - lib/astro_chart/pure/vsop87.rb
80
+ - lib/astro_chart/pure/vsop87_data.rb
81
+ - lib/astro_chart/synastry.rb
108
82
  - lib/astro_chart/time_conversion.rb
109
83
  - lib/astro_chart/version.rb
110
84
  - lib/astro_chart/zodiac.rb
111
85
  homepage: https://github.com/morriedig/astro_chart
112
86
  licenses:
113
- - AGPL-3.0
87
+ - MIT
114
88
  metadata: {}
115
89
  post_install_message:
116
90
  rdoc_options: []
@@ -130,5 +104,5 @@ requirements: []
130
104
  rubygems_version: 3.4.10
131
105
  signing_key:
132
106
  specification_version: 4
133
- summary: Natal chart calculation using Swiss Ephemeris
107
+ summary: Pure-Ruby natal chart calculation (planets, Placidus houses, aspects, synastry)
134
108
  test_files: []
@@ -1,99 +0,0 @@
1
- #include <ruby.h>
2
- #include "swephexp.h"
3
-
4
- static VALUE mAstroChart;
5
- static VALUE mExt;
6
-
7
- /*
8
- * AstroChart::Ext.julday(year, month, day, hour) -> Float
9
- *
10
- * Convert a date/time to Julian Day number using Gregorian calendar.
11
- */
12
- static VALUE rb_julday(VALUE self, VALUE year, VALUE month, VALUE day, VALUE hour) {
13
- int y = NUM2INT(year);
14
- int m = NUM2INT(month);
15
- int d = NUM2INT(day);
16
- double h = NUM2DBL(hour);
17
-
18
- double jd = swe_julday(y, m, d, h, SE_GREG_CAL);
19
- return DBL2NUM(jd);
20
- }
21
-
22
- /*
23
- * AstroChart::Ext.calc_ut(jd, planet_id) -> Float
24
- *
25
- * Calculate planet ecliptic longitude using Moshier ephemeris.
26
- * Returns the longitude in degrees (0-360).
27
- */
28
- static VALUE rb_calc_ut(VALUE self, VALUE jd, VALUE planet_id) {
29
- double tjd = NUM2DBL(jd);
30
- int ipl = NUM2INT(planet_id);
31
- double xx[6];
32
- char serr[256];
33
-
34
- int32 ret = swe_calc_ut(tjd, ipl, SEFLG_MOSEPH, xx, serr);
35
- if (ret < 0) {
36
- rb_raise(rb_eRuntimeError, "swe_calc_ut failed: %s", serr);
37
- }
38
-
39
- return DBL2NUM(xx[0]); /* ecliptic longitude */
40
- }
41
-
42
- /*
43
- * AstroChart::Ext.houses(jd, latitude, longitude, system) -> Hash
44
- *
45
- * Calculate house cusps and ascendant/MC.
46
- * system: ASCII code for house system (e.g. 'P' = 80 for Placidus)
47
- *
48
- * Returns a Hash with:
49
- * "cusps" => Array of 12 house cusp degrees
50
- * "ascendant" => Ascendant degree
51
- * "mc" => MC degree
52
- */
53
- static VALUE rb_houses(VALUE self, VALUE jd, VALUE lat, VALUE lon, VALUE hsys) {
54
- double tjd = NUM2DBL(jd);
55
- double geolat = NUM2DBL(lat);
56
- double geolon = NUM2DBL(lon);
57
- int system = NUM2INT(hsys);
58
-
59
- double cusps[13]; /* cusps[0] unused, cusps[1..12] */
60
- double ascmc[10];
61
-
62
- swe_houses(tjd, geolat, geolon, system, cusps, ascmc);
63
-
64
- VALUE result = rb_hash_new();
65
- VALUE cusps_ary = rb_ary_new_capa(12);
66
-
67
- int i;
68
- for (i = 1; i <= 12; i++) {
69
- rb_ary_push(cusps_ary, DBL2NUM(cusps[i]));
70
- }
71
-
72
- rb_hash_aset(result, rb_str_new_cstr("cusps"), cusps_ary);
73
- rb_hash_aset(result, rb_str_new_cstr("ascendant"), DBL2NUM(ascmc[0]));
74
- rb_hash_aset(result, rb_str_new_cstr("mc"), DBL2NUM(ascmc[1]));
75
-
76
- return result;
77
- }
78
-
79
- void Init_astro_chart_ext(void) {
80
- mAstroChart = rb_define_module("AstroChart");
81
- mExt = rb_define_module_under(mAstroChart, "Ext");
82
-
83
- rb_define_module_function(mExt, "julday", rb_julday, 4);
84
- rb_define_module_function(mExt, "calc_ut", rb_calc_ut, 2);
85
- rb_define_module_function(mExt, "houses", rb_houses, 4);
86
-
87
- /* Planet ID constants */
88
- rb_define_const(mExt, "SUN", INT2NUM(SE_SUN));
89
- rb_define_const(mExt, "MOON", INT2NUM(SE_MOON));
90
- rb_define_const(mExt, "MERCURY", INT2NUM(SE_MERCURY));
91
- rb_define_const(mExt, "VENUS", INT2NUM(SE_VENUS));
92
- rb_define_const(mExt, "MARS", INT2NUM(SE_MARS));
93
- rb_define_const(mExt, "JUPITER", INT2NUM(SE_JUPITER));
94
- rb_define_const(mExt, "SATURN", INT2NUM(SE_SATURN));
95
- rb_define_const(mExt, "URANUS", INT2NUM(SE_URANUS));
96
- rb_define_const(mExt, "NEPTUNE", INT2NUM(SE_NEPTUNE));
97
- rb_define_const(mExt, "PLUTO", INT2NUM(SE_PLUTO));
98
- rb_define_const(mExt, "TRUE_NODE", INT2NUM(SE_TRUE_NODE));
99
- }
@@ -1,8 +0,0 @@
1
- require "mkmf"
2
-
3
- # Swiss Ephemeris source files to compile alongside our extension
4
- $srcs = Dir.glob("#{$srcdir}/*.c").map { |f| File.basename(f) }
5
-
6
- $CFLAGS << " -DNO_SWE_GLP"
7
-
8
- create_makefile("astro_chart/astro_chart_ext")